Add network router and resource (#189)

This change adds two new resources, NetworkRouter and NetworkResource,
which enable clusters to expose Kubernetes services to Netbird.

The NetworkRouter is responsible for creating the network, group, setup
key and routing peer all of which are unique to the isntance. Along with
the deployment of the client in the cluster.

The NetworkResource exposes a service by linking to the specific router
it wants to expose to. This makes coupling between the resource and
network easy to understand.

Routers also set a DNS zone which is used to give names to resources
based on the name and namespace of the service being exposed.

Part of #172

Signed-off-by: Philip Laine <philip.laine@gmail.com>
This commit is contained in:
Philip Laine
2026-04-23 08:55:49 +02:00
committed by GitHub
parent af11e31b28
commit 6768a76c9c
38 changed files with 2973 additions and 55 deletions
+1 -1
View File
@@ -89,7 +89,7 @@ endif
.PHONY: install
install: manifests ## Install CRDs into the K8s cluster specified in ~/.kube/config.
$(KUBECTL) apply -f helm/kubernetes-operator/crds
$(KUBECTL) apply --server-side -f helm/kubernetes-operator/crds
.PHONY: uninstall
uninstall: manifests ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
+16
View File
@@ -91,4 +91,20 @@ resources:
kind: Group
path: github.com/netbirdio/kubernetes-operator/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: netbird.io
kind: NetworkRouter
path: github.com/netbirdio/kubernetes-operator/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: netbird.io
kind: NetworkResource
path: github.com/netbirdio/kubernetes-operator/api/v1alpha1
version: v1alpha1
version: "3"
+1
View File
@@ -6,4 +6,5 @@ const ReadyCondition = "Ready"
const (
ReconciledReason = "Reconciled"
DependencyReason = "Dependency"
)
+90
View File
@@ -0,0 +1,90 @@
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NetworkResourceSpec defines the desired state of NetworkResource.
type NetworkResourceSpec struct {
// NetworkRouterRef is a reference to the network and router where the resource will be created.
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
NetworkRouterRef CrossNamespaceReference `json:"networkRouterRef"`
// ServiceRef is a reference to the service to expose in the Network.
ServiceRef corev1.LocalObjectReference `json:"serviceRef"`
// Groups are references to groups that the resource will be a part of.
// +optional
Groups []ResourceReference `json:"groups,omitempty"`
}
// NetworkResourceStatus defines the observed state of NetworkResource.
type NetworkResourceStatus struct {
// ObservedGeneration is the last reconciled generation.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Conditions holds the conditions for the NetworkResource.
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
// NetworkID is the id of the network the resource is created in.
// +optional
NetworkID string `json:"networkID,omitempty"`
// ResourceID is the id of the created resource.
// +optional
ResourceID string `json:"resourceID,omitempty"`
// DNSZoneID is the id of the zone the DNS record is created in.
// +optional
DNSZoneID string `json:"dnsZoneID,omitempty"`
// DNSRecordID is the id of the created DNS record.
// +optional
DNSRecordID string `json:"dnsRecordID,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description=""
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
// NetworkResource is the Schema for the networkresources API.
type NetworkResource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// +required
Spec NetworkResourceSpec `json:"spec"`
// +kubebuilder:default={"observedGeneration":-1}
Status NetworkResourceStatus `json:"status,omitempty"`
}
// GetConditions returns the status conditions of the object.
func (n *NetworkResource) GetConditions() []metav1.Condition {
return n.Status.Conditions
}
// SetConditions sets the status conditions on the object.
func (n *NetworkResource) SetConditions(conditions []metav1.Condition) {
n.Status.Conditions = conditions
}
// +kubebuilder:object:root=true
// NetworkResourceList contains a list of NetworkResource.
type NetworkResourceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []NetworkResource `json:"items"`
}
func init() {
SchemeBuilder.Register(&NetworkResource{}, &NetworkResourceList{})
}
+106
View File
@@ -0,0 +1,106 @@
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NetworkRouterSpec defines the desired state of NetworkRouter.
type NetworkRouterSpec struct {
// DNSZoneRef is a reference to the DNS zone used to create records for resources.
// +required
DNSZoneRef DNSZoneReference `json:"dnsZoneRef"`
// WorkloadOverride contains configuration that will override the default workload.
// +optional
WorkloadOverride *WorkloadOverride `json:"workloadOverride,omitempty"`
}
// DNSZoneReference references a Netbird DNS zone by domain name.
type DNSZoneReference struct {
// Name is the domain name of an existing Netbird DNS zone, e.g. "example.com".
// +required
Name string `json:"name"`
}
type WorkloadOverride struct {
// Labels that will be added.
// +optional
Labels map[string]string `json:"labels"`
// Annotations that will be added.
// +optional
Annotations map[string]string `json:"annotations"`
// Replicas sets the amount of client replicas.
// +optional
Replicas *int32 `json:"replicas"`
// PodTemplate overrides the pod template.
// +optional
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:Schemaless
PodTemplate *corev1.PodTemplateSpec `json:"podTemplate"`
}
// NetworkRouterStatus defines the observed state of NetworkRouter.
type NetworkRouterStatus struct {
// ObservedGeneration is the last reconciled generation.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Conditions holds the conditions for the NetworkRouter.
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
// RoutingPeerID is the id of the created routing peer.
// +optional
RoutingPeerID string `json:"routingPeerID,omitempty"`
// NetworkID is the id of the network the routing peer was created in.
// +optional
NetworkID string `json:"networkID,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description=""
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
// NetworkRouter is the Schema for the networkrouters API.
type NetworkRouter struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// +required
Spec NetworkRouterSpec `json:"spec"`
// +kubebuilder:default={"observedGeneration":-1}
Status NetworkRouterStatus `json:"status,omitempty"`
}
// GetConditions returns the status conditions of the object.
func (n *NetworkRouter) GetConditions() []metav1.Condition {
return n.Status.Conditions
}
// SetConditions sets the status conditions on the object.
func (n *NetworkRouter) SetConditions(conditions []metav1.Condition) {
n.Status.Conditions = conditions
}
// +kubebuilder:object:root=true
// NetworkRouterList contains a list of NetworkRouter.
type NetworkRouterList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []NetworkRouter `json:"items"`
}
func init() {
SchemeBuilder.Register(&NetworkRouter{}, &NetworkRouterList{})
}
+12 -2
View File
@@ -4,11 +4,21 @@ import corev1 "k8s.io/api/core/v1"
// +kubebuilder:validation:XValidation:rule="(has(self.id) && !has(self.localRef)) || (!has(self.id) && has(self.localRef))",message="exactly one of id or localRef must be set"
type ResourceReference struct {
// id of the resource in the Netbird API.
// ID is the id of a resource in the Netbird API.
// +optional
ID *string `json:"id,omitempty"`
// local reference to the object in the same namespace.
// LocalReference is a reference to a object in the same namespace.
// +optional
LocalRef *corev1.LocalObjectReference `json:"localRef,omitempty"`
}
type CrossNamespaceReference struct {
// Name of the referent.
// +required
Name string `json:"name"`
// Namespace of the referent.
// +required
Namespace string `json:"namespace"`
}
+4
View File
@@ -6,6 +6,10 @@ import (
// SetupKeySpec defines the desired state of SetupKey.
type SetupKeySpec struct {
// Name of the setup key.
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
// Ephemeral decides if peers added with the key are ephemeral or not.
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="ephemeral is immutable"
Ephemeral bool `json:"ephemeral"`
+276
View File
@@ -10,6 +10,36 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CrossNamespaceReference) DeepCopyInto(out *CrossNamespaceReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossNamespaceReference.
func (in *CrossNamespaceReference) DeepCopy() *CrossNamespaceReference {
if in == nil {
return nil
}
out := new(CrossNamespaceReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DNSZoneReference) DeepCopyInto(out *DNSZoneReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSZoneReference.
func (in *DNSZoneReference) DeepCopy() *DNSZoneReference {
if in == nil {
return nil
}
out := new(DNSZoneReference)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Group) DeepCopyInto(out *Group) {
*out = *in
@@ -106,6 +136,213 @@ func (in *GroupStatus) DeepCopy() *GroupStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkResource) DeepCopyInto(out *NetworkResource) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkResource.
func (in *NetworkResource) DeepCopy() *NetworkResource {
if in == nil {
return nil
}
out := new(NetworkResource)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *NetworkResource) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkResourceList) DeepCopyInto(out *NetworkResourceList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]NetworkResource, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkResourceList.
func (in *NetworkResourceList) DeepCopy() *NetworkResourceList {
if in == nil {
return nil
}
out := new(NetworkResourceList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *NetworkResourceList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkResourceSpec) DeepCopyInto(out *NetworkResourceSpec) {
*out = *in
out.NetworkRouterRef = in.NetworkRouterRef
out.ServiceRef = in.ServiceRef
if in.Groups != nil {
in, out := &in.Groups, &out.Groups
*out = make([]ResourceReference, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkResourceSpec.
func (in *NetworkResourceSpec) DeepCopy() *NetworkResourceSpec {
if in == nil {
return nil
}
out := new(NetworkResourceSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkResourceStatus) DeepCopyInto(out *NetworkResourceStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkResourceStatus.
func (in *NetworkResourceStatus) DeepCopy() *NetworkResourceStatus {
if in == nil {
return nil
}
out := new(NetworkResourceStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkRouter) DeepCopyInto(out *NetworkRouter) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkRouter.
func (in *NetworkRouter) DeepCopy() *NetworkRouter {
if in == nil {
return nil
}
out := new(NetworkRouter)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *NetworkRouter) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkRouterList) DeepCopyInto(out *NetworkRouterList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]NetworkRouter, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkRouterList.
func (in *NetworkRouterList) DeepCopy() *NetworkRouterList {
if in == nil {
return nil
}
out := new(NetworkRouterList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *NetworkRouterList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkRouterSpec) DeepCopyInto(out *NetworkRouterSpec) {
*out = *in
out.DNSZoneRef = in.DNSZoneRef
if in.WorkloadOverride != nil {
in, out := &in.WorkloadOverride, &out.WorkloadOverride
*out = new(WorkloadOverride)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkRouterSpec.
func (in *NetworkRouterSpec) DeepCopy() *NetworkRouterSpec {
if in == nil {
return nil
}
out := new(NetworkRouterSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NetworkRouterStatus) DeepCopyInto(out *NetworkRouterStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkRouterStatus.
func (in *NetworkRouterStatus) DeepCopy() *NetworkRouterStatus {
if in == nil {
return nil
}
out := new(NetworkRouterStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourceReference) DeepCopyInto(out *ResourceReference) {
*out = *in
@@ -238,3 +475,42 @@ func (in *SetupKeyStatus) DeepCopy() *SetupKeyStatus {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WorkloadOverride) DeepCopyInto(out *WorkloadOverride) {
*out = *in
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Replicas != nil {
in, out := &in.Replicas, &out.Replicas
*out = new(int32)
**out = **in
}
if in.PodTemplate != nil {
in, out := &in.PodTemplate, &out.PodTemplate
*out = new(corev1.PodTemplateSpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadOverride.
func (in *WorkloadOverride) DeepCopy() *WorkloadOverride {
if in == nil {
return nil
}
out := new(WorkloadOverride)
in.DeepCopyInto(out)
return out
}
+29 -9
View File
@@ -29,7 +29,7 @@ import (
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
netbirdrest "github.com/netbirdio/netbird/shared/management/client/rest"
netbird "github.com/netbirdio/netbird/shared/management/client/rest"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -222,11 +222,15 @@ func main() {
}
if len(netbirdAPIKey) > 0 {
netbird := netbirdrest.New(managementURL, netbirdAPIKey)
nbClient := netbird.NewWithOptions(
netbird.WithManagementURL(managementURL),
netbird.WithBearerToken(netbirdAPIKey),
netbird.WithUserAgent("netbird-operator"),
)
if err = (&controller.NBRoutingPeerReconciler{
Client: mgr.GetClient(),
Netbird: netbird,
Netbird: nbClient,
ClientImage: clientImage,
ClusterName: clusterName,
ManagementURL: managementURL,
@@ -251,7 +255,7 @@ func main() {
if err = (&controller.NBResourceReconciler{
Client: mgr.GetClient(),
Netbird: netbird,
Netbird: nbClient,
AllowAutomaticPolicyCreation: allowAutomaticPolicyCreation,
ClusterName: clusterName,
DefaultLabels: defaultLabelsMap,
@@ -262,7 +266,7 @@ func main() {
if err = (&controller.NBGroupReconciler{
Client: mgr.GetClient(),
Netbird: netbird,
Netbird: nbClient,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "NBGroup")
os.Exit(1)
@@ -270,7 +274,7 @@ func main() {
if err = (&controller.NBPolicyReconciler{
Client: mgr.GetClient(),
Netbird: netbird,
Netbird: nbClient,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "NBPolicy")
os.Exit(1)
@@ -285,18 +289,34 @@ func main() {
if err := (&controller.SetupKeyReconciler{
Client: mgr.GetClient(),
Netbird: netbird,
Netbird: nbClient,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "Failed to create controller", "controller", "SetupKey")
os.Exit(1)
}
if err := (&controller.GroupReconciler{
Client: mgr.GetClient(),
Netbird: netbird,
Netbird: nbClient,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "Failed to create controller", "controller", "Group")
os.Exit(1)
}
if err := (&controller.NetworkRouterReconciler{
Client: mgr.GetClient(),
Netbird: nbClient,
ClientImage: clientImage,
ManagementURL: managementURL,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "Failed to create controller", "controller", "NetworkRouter")
os.Exit(1)
}
if err := (&controller.NetworkResourceReconciler{
Client: mgr.GetClient(),
Netbird: nbClient,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "Failed to create controller", "controller", "NetworkResource")
os.Exit(1)
}
if gatewayAPIEnabled {
if err = (&controller.GatewayClassReconciler{
@@ -313,7 +333,7 @@ func main() {
}
if err = (&controller.HTTPRouteReconciler{
Client: mgr.GetClient(),
Netbird: netbird,
Netbird: nbClient,
ClusterDNS: clusterDNS,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "HTTPRoute")
+1 -1
View File
@@ -2,6 +2,6 @@ apiVersion: netbird.io/v1alpha1
kind: Group
metadata:
name: test
namespace: default
namespace: netbird
spec:
name: test
+55
View File
@@ -0,0 +1,55 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: default
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
imagePullPolicy: Always
name: nginx
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: default
labels:
app: nginx
spec:
type: ClusterIP
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
selector:
app: nginx
---
apiVersion: netbird.io/v1alpha1
kind: NetworkResource
metadata:
name: test
namespace: default
spec:
networkRouterRef:
name: test
namespace: netbird
serviceRef:
name: nginx
+8
View File
@@ -0,0 +1,8 @@
apiVersion: netbird.io/v1alpha1
kind: NetworkRouter
metadata:
name: test
namespace: netbird
spec:
dnsZoneRef:
name: cluster.local
+2 -2
View File
@@ -2,6 +2,6 @@ apiVersion: netbird.io/v1alpha1
kind: SetupKey
metadata:
name: test
namespace: default
namespace: netbird
spec:
ephemeral: true
name: test
@@ -0,0 +1,204 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.20.1
name: networkresources.netbird.io
spec:
group: netbird.io
names:
kind: NetworkResource
listKind: NetworkResourceList
plural: networkresources
singular: networkresource
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: NetworkResource is the Schema for the networkresources API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: NetworkResourceSpec defines the desired state of NetworkResource.
properties:
groups:
description: Groups are references to groups that the resource will
be a part of.
items:
properties:
id:
description: ID is the id of a resource in the Netbird API.
type: string
localRef:
description: LocalReference is a reference to a object in the
same namespace.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
type: object
x-kubernetes-validations:
- message: exactly one of id or localRef must be set
rule: (has(self.id) && !has(self.localRef)) || (!has(self.id)
&& has(self.localRef))
type: array
networkRouterRef:
description: NetworkRouterRef is a reference to the network and router
where the resource will be created.
properties:
name:
description: Name of the referent.
type: string
namespace:
description: Namespace of the referent.
type: string
required:
- name
- namespace
type: object
x-kubernetes-validations:
- message: Value is immutable
rule: self == oldSelf
serviceRef:
description: ServiceRef is a reference to the service to expose in
the Network.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
required:
- networkRouterRef
- serviceRef
type: object
status:
default:
observedGeneration: -1
description: NetworkResourceStatus defines the observed state of NetworkResource.
properties:
conditions:
description: Conditions holds the conditions for the NetworkResource.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
dnsRecordID:
description: DNSRecordID is the id of the created DNS record.
type: string
dnsZoneID:
description: DNSZoneID is the id of the zone the DNS record is created
in.
type: string
networkID:
description: NetworkID is the id of the network the resource is created
in.
type: string
observedGeneration:
description: ObservedGeneration is the last reconciled generation.
format: int64
type: integer
resourceID:
description: ResourceID is the id of the created resource.
type: string
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
@@ -0,0 +1,168 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.20.1
name: networkrouters.netbird.io
spec:
group: netbird.io
names:
kind: NetworkRouter
listKind: NetworkRouterList
plural: networkrouters
singular: networkrouter
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: NetworkRouter is the Schema for the networkrouters API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: NetworkRouterSpec defines the desired state of NetworkRouter.
properties:
dnsZoneRef:
description: DNSZoneRef is a reference to the DNS zone used to create
records for resources.
properties:
name:
description: Name is the domain name of an existing Netbird DNS
zone, e.g. "example.com".
type: string
required:
- name
type: object
workloadOverride:
description: WorkloadOverride contains configuration that will override
the default workload.
properties:
annotations:
additionalProperties:
type: string
description: Annotations that will be added.
type: object
labels:
additionalProperties:
type: string
description: Labels that will be added.
type: object
podTemplate:
description: PodTemplate overrides the pod template.
x-kubernetes-preserve-unknown-fields: true
replicas:
description: Replicas sets the amount of client replicas.
format: int32
type: integer
type: object
required:
- dnsZoneRef
type: object
status:
default:
observedGeneration: -1
description: NetworkRouterStatus defines the observed state of NetworkRouter.
properties:
conditions:
description: Conditions holds the conditions for the NetworkRouter.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
networkID:
description: NetworkID is the id of the network the routing peer was
created in.
type: string
observedGeneration:
description: ObservedGeneration is the last reconciled generation.
format: int64
type: integer
routingPeerID:
description: RoutingPeerID is the id of the created routing peer.
type: string
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
@@ -52,10 +52,11 @@ spec:
items:
properties:
id:
description: id of the resource in the Netbird API.
description: ID is the id of a resource in the Netbird API.
type: string
localRef:
description: local reference to the object in the same namespace.
description: LocalReference is a reference to a object in the
same namespace.
properties:
name:
default: ""
@@ -87,8 +88,13 @@ spec:
x-kubernetes-validations:
- message: ephemeral is immutable
rule: self == oldSelf
name:
description: Name of the setup key.
minLength: 1
type: string
required:
- ephemeral
- name
type: object
status:
default:
@@ -0,0 +1,301 @@
package controller
import (
"context"
"fmt"
"strings"
"time"
"github.com/fluxcd/pkg/runtime/conditions"
"github.com/fluxcd/pkg/runtime/patch"
netbird "github.com/netbirdio/netbird/shared/management/client/rest"
"github.com/netbirdio/netbird/shared/management/http/api"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
nbv1alpha1 "github.com/netbirdio/kubernetes-operator/api/v1alpha1"
"github.com/netbirdio/kubernetes-operator/internal/netbirdutil"
)
type NetworkResourceReconciler struct {
client.Client
Netbird *netbird.Client
}
// +kubebuilder:rbac:groups=netbird.io,resources=networkresources,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=netbird.io,resources=networkresources/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=netbird.io,resources=networkresources/finalizers,verbs=update
// nolint:gocyclo
func (r *NetworkResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
netResource := &nbv1alpha1.NetworkResource{}
err := r.Get(ctx, req.NamespacedName, netResource)
if err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
sp := patch.NewSerialPatcher(netResource, r.Client)
if !netResource.DeletionTimestamp.IsZero() {
return r.reconcileDelete(ctx, sp, netResource)
}
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: netResource.Spec.ServiceRef.Name,
Namespace: netResource.Namespace,
},
}
err = r.Get(ctx, client.ObjectKeyFromObject(svc), svc)
if err != nil {
if kerrors.IsNotFound(err) {
conditions.MarkFalse(netResource, nbv1alpha1.ReadyCondition, nbv1alpha1.DependencyReason, "Referenced Service cannot be found.")
err = sp.Patch(ctx, netResource)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
if svc.Spec.Type != corev1.ServiceTypeClusterIP {
conditions.MarkFalse(netResource, nbv1alpha1.ReadyCondition, nbv1alpha1.DependencyReason, "Referenced Service is not of type ClusterIP.")
err = sp.Patch(ctx, netResource)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
if svc.Spec.ClusterIP == "" || svc.Spec.ClusterIP == corev1.ClusterIPNone {
conditions.MarkFalse(netResource, nbv1alpha1.ReadyCondition, nbv1alpha1.DependencyReason, "Referenced Service does not have a ClusterIP set.")
err = sp.Patch(ctx, netResource)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
netRouter := &nbv1alpha1.NetworkRouter{
ObjectMeta: metav1.ObjectMeta{
Name: netResource.Spec.NetworkRouterRef.Name,
Namespace: netResource.Spec.NetworkRouterRef.Namespace,
},
}
err = r.Get(ctx, client.ObjectKeyFromObject(netRouter), netRouter)
if err != nil {
if kerrors.IsNotFound(err) {
conditions.MarkFalse(netResource, nbv1alpha1.ReadyCondition, nbv1alpha1.DependencyReason, "Referenced NetworkRouter cannot be found.")
err = sp.Patch(ctx, netResource)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
if netRouter.Status.NetworkID == "" || netRouter.Status.RoutingPeerID == "" {
conditions.MarkFalse(netResource, nbv1alpha1.ReadyCondition, nbv1alpha1.DependencyReason, "Referenced NetworkRouter is not ready.")
err = sp.Patch(ctx, netResource)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
groupIDs, err := netbirdutil.GetGroupIDs(ctx, r.Client, r.Netbird, netResource.Spec.Groups, netResource.Namespace)
if err != nil {
return ctrl.Result{}, err
}
controllerutil.AddFinalizer(netResource, nbv1alpha1.NetbirdFinalizer)
resourceID, err := func() (string, error) {
netReq := api.NetworkResourceRequest{
Name: svc.Name + "/" + svc.Namespace,
Address: svc.Spec.ClusterIP,
Enabled: true,
Groups: groupIDs,
}
if netResource.Status.ResourceID != "" {
netResp, err := r.Netbird.Networks.Resources(netRouter.Status.NetworkID).Update(ctx, netResource.Status.ResourceID, netReq)
if err != nil && !netbird.IsNotFound(err) {
return "", err
}
if err == nil {
return netResp.Id, nil
}
}
netResp, err := r.Netbird.Networks.Resources(netRouter.Status.NetworkID).Create(ctx, netReq)
if err != nil {
return "", err
}
return netResp.Id, nil
}()
if err != nil {
return ctrl.Result{}, err
}
netResource.Status.NetworkID = netRouter.Status.NetworkID
netResource.Status.ResourceID = resourceID
err = sp.Patch(ctx, netResource)
if err != nil {
return ctrl.Result{}, err
}
// Create DNS records for resource.
zone, err := netbirdutil.GetDNSZoneByName(ctx, r.Netbird, netRouter.Spec.DNSZoneRef.Name)
if err != nil {
return ctrl.Result{}, err
}
// If zone has changed we need to delete the old records.
if netResource.Status.DNSZoneID != "" && netResource.Status.DNSZoneID != zone.Id {
err = r.Netbird.DNSZones.DeleteRecord(ctx, netResource.Status.DNSZoneID, netResource.Status.DNSRecordID)
if err != nil && !netbird.IsNotFound(err) {
return ctrl.Result{}, err
}
netResource.Status.DNSZoneID = ""
netResource.Status.DNSRecordID = ""
}
recordID, err := func() (string, error) {
dnsReq := api.DNSRecordRequest{
Content: svc.Spec.ClusterIP,
Name: strings.Join([]string{svc.Name, svc.Namespace, zone.Name}, "."),
Ttl: int(5 * time.Minute / time.Second),
Type: api.DNSRecordTypeA,
}
if netResource.Status.DNSZoneID != "" && netResource.Status.DNSRecordID != "" {
recordResp, err := r.Netbird.DNSZones.UpdateRecord(ctx, netResource.Status.DNSZoneID, netResource.Status.DNSRecordID, dnsReq)
if err != nil && !netbird.IsNotFound(err) {
return "", err
}
if err == nil {
return recordResp.Id, nil
}
}
recordResp, err := r.Netbird.DNSZones.CreateRecord(ctx, zone.Id, dnsReq)
if err != nil {
return "", err
}
return recordResp.Id, nil
}()
if err != nil {
return ctrl.Result{}, err
}
netResource.Status.DNSZoneID = zone.Id
netResource.Status.DNSRecordID = recordID
conditions.MarkTrue(netResource, nbv1alpha1.ReadyCondition, nbv1alpha1.ReconciledReason, "")
err = sp.Patch(ctx, netResource, patch.WithStatusObservedGeneration{})
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func (r *NetworkResourceReconciler) reconcileDelete(ctx context.Context, sp *patch.SerialPatcher, netResource *nbv1alpha1.NetworkResource) (ctrl.Result, error) {
if netResource.Status.NetworkID != "" && netResource.Status.ResourceID != "" {
err := r.Netbird.Networks.Resources(netResource.Status.NetworkID).Delete(ctx, netResource.Status.ResourceID)
if err != nil && !netbird.IsNotFound(err) {
return ctrl.Result{}, err
}
}
if netResource.Status.DNSZoneID != "" && netResource.Status.DNSRecordID != "" {
err := r.Netbird.DNSZones.DeleteRecord(ctx, netResource.Status.DNSZoneID, netResource.Status.DNSRecordID)
if err != nil && !netbird.IsNotFound(err) {
return ctrl.Result{}, err
}
}
controllerutil.RemoveFinalizer(netResource, nbv1alpha1.NetbirdFinalizer)
err := sp.Patch(ctx, netResource)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func (r *NetworkResourceReconciler) SetupWithManager(mgr ctrl.Manager) error {
err := mgr.GetFieldIndexer().IndexField(context.Background(), &nbv1alpha1.NetworkResource{}, ".spec.networkRouterRef", func(obj client.Object) []string {
netResource := obj.(*nbv1alpha1.NetworkResource)
ref := netResource.Spec.NetworkRouterRef
if ref.Name == "" {
return nil
}
if ref.Namespace == "" {
ref.Namespace = netResource.Namespace
}
return []string{fmt.Sprintf("%s/%s", ref.Name, ref.Namespace)}
})
if err != nil {
return err
}
err = mgr.GetFieldIndexer().IndexField(context.Background(), &nbv1alpha1.NetworkResource{}, ".spec.serviceRef", func(obj client.Object) []string {
netResource := obj.(*nbv1alpha1.NetworkResource)
ref := netResource.Spec.ServiceRef
if ref.Name == "" {
return nil
}
return []string{netResource.Spec.ServiceRef.Name}
})
if err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
For(&nbv1alpha1.NetworkResource{}).
Watches(
&nbv1alpha1.NetworkRouter{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
netResourceList := &nbv1alpha1.NetworkResourceList{}
err := r.List(ctx, netResourceList, client.MatchingFields{".spec.networkRouterRef": fmt.Sprintf("%s/%s", obj.GetName(), obj.GetNamespace())})
if err != nil {
return nil
}
requests := make([]reconcile.Request, len(netResourceList.Items))
for i, item := range netResourceList.Items {
requests[i] = reconcile.Request{
NamespacedName: types.NamespacedName{
Name: item.Name,
Namespace: item.Namespace,
},
}
}
return requests
}),
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
).
Watches(
&corev1.Service{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
netResourceList := &nbv1alpha1.NetworkResourceList{}
err := r.List(ctx, netResourceList, client.InNamespace(obj.GetNamespace()), client.MatchingFields{".spec.serviceRef": obj.GetName()})
if err != nil {
return nil
}
requests := make([]reconcile.Request, len(netResourceList.Items))
for i, item := range netResourceList.Items {
requests[i] = reconcile.Request{
NamespacedName: types.NamespacedName{
Name: item.Name,
Namespace: item.Namespace,
},
}
}
return requests
}),
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
).
Complete(r)
}
@@ -0,0 +1,150 @@
package controller
import (
"context"
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
nbv1alpha1 "github.com/netbirdio/kubernetes-operator/api/v1alpha1"
"github.com/netbirdio/kubernetes-operator/internal/netbirdmock"
"github.com/netbirdio/netbird/shared/management/http/api"
)
var _ = Describe("NetworkResource Controller", func() {
Context("When reconciling a resource", func() {
ctx := context.Background()
var netResourceRec *NetworkResourceReconciler
var netRouterRec *NetworkRouterReconciler
var setupKeyRec *SetupKeyReconciler
var groupRec *GroupReconciler
nn := client.ObjectKey{
Name: "test-resource",
Namespace: "network-resource",
}
BeforeEach(func() {
nbClient := netbirdmock.Client()
netResourceRec = &NetworkResourceReconciler{
Client: k8sClient,
Netbird: nbClient,
}
netRouterRec = &NetworkRouterReconciler{
Client: k8sClient,
Netbird: nbClient,
ClientImage: "docker.io/netbirdio/netbird:latest",
ManagementURL: "https://netbird.io",
}
setupKeyRec = &SetupKeyReconciler{
Client: k8sClient,
Netbird: nbClient,
}
groupRec = &GroupReconciler{
Client: k8sClient,
Netbird: nbClient,
}
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nn.Namespace,
},
}
Expect(k8sClient.Create(ctx, ns)).To(Succeed())
})
AfterEach(func() {
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nn.Namespace,
},
}
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(ns), ns)
if kerrors.IsNotFound(err) {
return
}
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient.Delete(ctx, ns)).To(Succeed())
})
It("creates a network resource and DNS record", func() {
zoneReq := api.ZoneRequest{
Name: "cluster.local",
Domain: "cluster.local",
}
_, err := netRouterRec.Netbird.DNSZones.CreateZone(ctx, zoneReq)
Expect(err).ToNot(HaveOccurred())
// Create network router that we reference.
netRouter := &nbv1alpha1.NetworkRouter{
ObjectMeta: metav1.ObjectMeta{
Name: nn.Name,
Namespace: nn.Namespace,
},
Spec: nbv1alpha1.NetworkRouterSpec{
DNSZoneRef: nbv1alpha1.DNSZoneReference{
Name: "cluster.local",
},
},
}
Expect(k8sClient.Create(ctx, netRouter)).To(Succeed())
for range 3 {
_, err := netRouterRec.Reconcile(ctx, reconcile.Request{NamespacedName: nn})
Expect(err).NotTo(HaveOccurred())
key := client.ObjectKey{Name: fmt.Sprintf("networkrouter-%s", netRouter.Name), Namespace: nn.Namespace}
_, err = groupRec.Reconcile(ctx, reconcile.Request{NamespacedName: key})
Expect(err).NotTo(HaveOccurred())
_, err = setupKeyRec.Reconcile(ctx, reconcile.Request{NamespacedName: key})
Expect(err).NotTo(HaveOccurred())
}
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: nn.Namespace,
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Port: 8080,
},
},
},
}
Expect(k8sClient.Create(ctx, svc)).To(Succeed())
netResource := &nbv1alpha1.NetworkResource{
ObjectMeta: metav1.ObjectMeta{
Name: nn.Name,
Namespace: nn.Namespace,
},
Spec: nbv1alpha1.NetworkResourceSpec{
NetworkRouterRef: nbv1alpha1.CrossNamespaceReference{
Name: netRouter.Name,
Namespace: netRouter.Namespace,
},
ServiceRef: corev1.LocalObjectReference{
Name: svc.Name,
},
},
}
Expect(k8sClient.Create(ctx, netResource)).To(Succeed())
_, err = netResourceRec.Reconcile(ctx, reconcile.Request{NamespacedName: nn})
Expect(err).NotTo(HaveOccurred())
err = k8sClient.Get(ctx, nn, netResource)
Expect(err).NotTo(HaveOccurred())
Expect(netResource.Status.NetworkID).NotTo(BeEmpty())
Expect(netResource.Status.ResourceID).NotTo(BeEmpty())
Expect(netResource.Status.DNSZoneID).NotTo(BeEmpty())
Expect(netResource.Status.DNSRecordID).NotTo(BeEmpty())
})
})
})
@@ -0,0 +1,322 @@
package controller
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"maps"
"time"
"github.com/fluxcd/pkg/runtime/conditions"
"github.com/fluxcd/pkg/runtime/patch"
"github.com/netbirdio/kubernetes-operator/internal/netbirdutil"
"github.com/netbirdio/kubernetes-operator/internal/ssautil"
netbird "github.com/netbirdio/netbird/shared/management/client/rest"
"github.com/netbirdio/netbird/shared/management/http/api"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/strategicpatch"
appsv1ac "k8s.io/client-go/applyconfigurations/apps/v1"
corev1ac "k8s.io/client-go/applyconfigurations/core/v1"
metav1ac "k8s.io/client-go/applyconfigurations/meta/v1"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
nbv1alpha1 "github.com/netbirdio/kubernetes-operator/api/v1alpha1"
nbv1alpha1ac "github.com/netbirdio/kubernetes-operator/pkg/applyconfigurations/api/v1alpha1"
)
type NetworkRouterReconciler struct {
client.Client
Netbird *netbird.Client
ManagementURL string
ClientImage string
}
// +kubebuilder:rbac:groups=netbird.io,resources=networkrouters,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=netbird.io,resources=networkrouters/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=netbird.io,resources=networkrouters/finalizers,verbs=update
// nolint:gocyclo
func (r *NetworkRouterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
netRouter := &nbv1alpha1.NetworkRouter{}
err := r.Get(ctx, req.NamespacedName, netRouter)
if err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
sp := patch.NewSerialPatcher(netRouter, r.Client)
if !netRouter.DeletionTimestamp.IsZero() {
return r.reconcileDelete(ctx, sp, netRouter)
}
ownerRef, err := ssautil.OwnerReference(netRouter, r.Scheme())
if err != nil {
return ctrl.Result{}, err
}
// Ensure the DNS Zone exists.
_, err = netbirdutil.GetDNSZoneByName(ctx, r.Netbird, netRouter.Spec.DNSZoneRef.Name)
if err != nil {
return ctrl.Result{}, err
}
controllerutil.AddFinalizer(netRouter, nbv1alpha1.NetbirdFinalizer)
networkID, err := func() (string, error) {
networkReq := api.NetworkRequest{
Name: netRouter.Name,
}
if netRouter.Status.NetworkID != "" {
networkResp, err := r.Netbird.Networks.Update(ctx, netRouter.Status.NetworkID, networkReq)
if err != nil && !netbird.IsNotFound(err) {
return "", err
}
if err == nil {
return networkResp.Id, nil
}
}
networkResp, err := r.Netbird.Networks.Create(ctx, networkReq)
if err != nil {
return "", err
}
return networkResp.Id, nil
}()
if err != nil {
return ctrl.Result{}, err
}
netRouter.Status.NetworkID = networkID
err = sp.Patch(ctx, netRouter)
if err != nil {
return ctrl.Result{}, err
}
// Calculate unique suffix used for Netbird resources.
sum := sha256.Sum256([]byte(netRouter.UID))
uniqueSuffix := networkID + "-" + fmt.Sprintf("%x", sum[:4])[:8]
// Create the group used by the router to discover peers.
groupAC := nbv1alpha1ac.Group(fmt.Sprintf("networkrouter-%s", netRouter.Name), req.Namespace).
WithOwnerReferences(ownerRef).
WithSpec(
nbv1alpha1ac.GroupSpec().
WithName(fmt.Sprintf("networkrouter-%s", uniqueSuffix)),
)
err = r.Client.Apply(ctx, groupAC)
if err != nil {
return ctrl.Result{}, err
}
group := &nbv1alpha1.Group{
ObjectMeta: metav1.ObjectMeta{
Name: *groupAC.Name,
Namespace: *groupAC.Namespace,
},
}
err = r.Client.Get(ctx, client.ObjectKeyFromObject(group), group)
if err != nil {
return ctrl.Result{}, err
}
if group.Status.GroupID == "" {
return ctrl.Result{}, nil
}
// Create the setup key used by routing peers.
setupKeyAC := nbv1alpha1ac.SetupKey(fmt.Sprintf("networkrouter-%s", netRouter.Name), req.Namespace).
WithOwnerReferences(ownerRef).
WithSpec(
nbv1alpha1ac.SetupKeySpec().
WithName(fmt.Sprintf("networkrouter-%s", uniqueSuffix)).
WithEphemeral(true).
WithAutoGroups(nbv1alpha1ac.ResourceReference().WithID(group.Status.GroupID)),
)
err = r.Client.Apply(ctx, setupKeyAC)
if err != nil {
return ctrl.Result{}, err
}
setupKey := nbv1alpha1.SetupKey{
ObjectMeta: metav1.ObjectMeta{
Name: *setupKeyAC.Name,
Namespace: *setupKeyAC.Namespace,
},
}
err = r.Get(ctx, client.ObjectKeyFromObject(&setupKey), &setupKey)
if err != nil {
return ctrl.Result{}, err
}
if setupKey.Status.SetupKeyID == "" {
return ctrl.Result{}, nil
}
// Create the routing peer in netbird.
routingPeerID, err := func() (string, error) {
routerReq := api.NetworkRouterRequest{
Enabled: true,
Masquerade: true,
Metric: 9999,
PeerGroups: ptr.To([]string{group.Status.GroupID}),
}
if netRouter.Status.RoutingPeerID != "" {
resp, err := r.Netbird.Networks.Routers(networkID).Update(ctx, netRouter.Status.RoutingPeerID, routerReq)
if err != nil && !netbird.IsNotFound(err) {
return "", err
}
if err == nil {
return resp.Id, nil
}
}
resp, err := r.Netbird.Networks.Routers(networkID).Create(ctx, routerReq)
if err != nil {
return "", err
}
return resp.Id, nil
}()
if err != nil {
return ctrl.Result{}, err
}
netRouter.Status.RoutingPeerID = routingPeerID
err = sp.Patch(ctx, netRouter, patch.WithStatusObservedGeneration{})
if err != nil {
return ctrl.Result{}, err
}
// Create the deployment.
selectorLabels := map[string]string{
"app.kubernetes.io/name": "networkrouter",
"app.kubernetes.io/instance": req.Name,
}
podTemplateSpecAC := corev1ac.PodTemplateSpec().
WithLabels(selectorLabels).
WithSpec(corev1ac.PodSpec().
WithContainers(corev1ac.Container().
WithName("netbird").
WithImage(r.ClientImage).
WithEnv(
corev1ac.EnvVar().
WithName("NB_SETUP_KEY").
WithValueFrom(corev1ac.EnvVarSource().
WithSecretKeyRef(corev1ac.SecretKeySelector().
WithName(setupKey.SecretName()).
WithKey(SetupKeySecretKey),
),
),
corev1ac.EnvVar().
WithName("NB_MANAGEMENT_URL").
WithValue(r.ManagementURL),
corev1ac.EnvVar().
WithName("NB_LOG_LEVEL").
WithValue("info"),
).
WithStartupProbe(corev1ac.Probe().WithExec(corev1ac.ExecAction().WithCommand("netbird", "status", "--check", "startup"))).
WithReadinessProbe(corev1ac.Probe().WithExec(corev1ac.ExecAction().WithCommand("netbird", "status", "--check", "ready"))).
WithSecurityContext(corev1ac.SecurityContext().
WithCapabilities(corev1ac.Capabilities().
WithAdd("NET_ADMIN").
WithAdd("SYS_RESOURCE").
WithAdd("SYS_ADMIN"),
).
WithPrivileged(true),
),
),
)
depLabels := map[string]string{}
depAnnotations := map[string]string{}
replicas := int32(3)
if netRouter.Spec.WorkloadOverride != nil {
if netRouter.Spec.WorkloadOverride.Labels != nil {
depLabels = netRouter.Spec.WorkloadOverride.Labels
}
if netRouter.Spec.WorkloadOverride.Annotations != nil {
depAnnotations = netRouter.Spec.WorkloadOverride.Annotations
}
if netRouter.Spec.WorkloadOverride.Replicas != nil {
replicas = *netRouter.Spec.WorkloadOverride.Replicas
}
if netRouter.Spec.WorkloadOverride.PodTemplate != nil {
baseJSON, err := json.Marshal(&podTemplateSpecAC)
if err != nil {
return ctrl.Result{}, err
}
overrideJSON, err := json.Marshal(netRouter.Spec.WorkloadOverride.PodTemplate)
if err != nil {
return ctrl.Result{}, err
}
mergedJSON, err := strategicpatch.StrategicMergePatch(baseJSON, overrideJSON, corev1.PodTemplateSpec{})
if err != nil {
return ctrl.Result{}, err
}
err = json.Unmarshal(mergedJSON, &podTemplateSpecAC)
if err != nil {
return ctrl.Result{}, err
}
}
}
maps.Copy(depLabels, selectorLabels)
depAC := appsv1ac.Deployment(fmt.Sprintf("networkrouter-%s", req.Name), req.Namespace).
WithOwnerReferences(ownerRef).
WithLabels(depLabels).
WithAnnotations(depAnnotations).
WithSpec(appsv1ac.DeploymentSpec().WithReplicas(replicas).WithSelector(metav1ac.LabelSelector().WithMatchLabels(selectorLabels)).WithTemplate(podTemplateSpecAC))
err = r.Client.Apply(ctx, depAC)
if err != nil {
return ctrl.Result{}, err
}
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: *depAC.Name,
Namespace: *depAC.Namespace,
},
}
err = r.Client.Get(ctx, client.ObjectKeyFromObject(dep), dep)
if err != nil {
return ctrl.Result{}, err
}
if dep.Status.ReadyReplicas != dep.Status.Replicas {
return ctrl.Result{}, nil
}
conditions.MarkTrue(netRouter, nbv1alpha1.ReadyCondition, nbv1alpha1.ReconciledReason, "")
err = sp.Patch(ctx, netRouter, patch.WithStatusObservedGeneration{})
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: 15 * time.Minute}, nil
}
func (r *NetworkRouterReconciler) reconcileDelete(ctx context.Context, sp *patch.SerialPatcher, netRouter *nbv1alpha1.NetworkRouter) (ctrl.Result, error) {
if netRouter.Status.RoutingPeerID != "" {
err := r.Netbird.Networks.Routers(netRouter.Status.NetworkID).Delete(ctx, netRouter.Status.RoutingPeerID)
if err != nil && !netbird.IsNotFound(err) {
return ctrl.Result{}, err
}
}
if netRouter.Status.NetworkID != "" {
err := r.Netbird.Networks.Delete(ctx, netRouter.Status.NetworkID)
if err != nil && !netbird.IsNotFound(err) {
return ctrl.Result{}, err
}
}
controllerutil.RemoveFinalizer(netRouter, nbv1alpha1.NetbirdFinalizer)
err := sp.Patch(ctx, netRouter)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func (r *NetworkRouterReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&nbv1alpha1.NetworkRouter{}).
Owns(&nbv1alpha1.Group{}).
Owns(&nbv1alpha1.SetupKey{}).
Owns(&appsv1.Deployment{}).
Complete(r)
}
@@ -0,0 +1,147 @@
package controller
import (
"context"
"fmt"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
nbv1alpha1 "github.com/netbirdio/kubernetes-operator/api/v1alpha1"
"github.com/netbirdio/kubernetes-operator/internal/netbirdmock"
"github.com/netbirdio/netbird/shared/management/http/api"
)
var _ = Describe("NetworkRouter Controller", func() {
Context("When reconciling a resource", func() {
ctx := context.Background()
var netRouterRec *NetworkRouterReconciler
var setupKeyRec *SetupKeyReconciler
var groupRec *GroupReconciler
nn := client.ObjectKey{
Name: "test-resource",
Namespace: "network-router",
}
BeforeEach(func() {
nbClient := netbirdmock.Client()
netRouterRec = &NetworkRouterReconciler{
Client: k8sClient,
Netbird: nbClient,
ClientImage: "docker.io/netbirdio/netbird:latest",
ManagementURL: "https://netbird.io",
}
setupKeyRec = &SetupKeyReconciler{
Client: k8sClient,
Netbird: nbClient,
}
groupRec = &GroupReconciler{
Client: k8sClient,
Netbird: nbClient,
}
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nn.Namespace,
},
}
Expect(k8sClient.Create(ctx, ns)).To(Succeed())
})
AfterEach(func() {
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nn.Namespace,
},
}
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(ns), ns)
if kerrors.IsNotFound(err) {
return
}
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient.Delete(ctx, ns)).To(Succeed())
})
It("creates a routing peer along with a deployment", func() {
zoneReq := api.ZoneRequest{
Name: "cluster.local",
Domain: "cluster.local",
}
_, err := netRouterRec.Netbird.DNSZones.CreateZone(ctx, zoneReq)
Expect(err).ToNot(HaveOccurred())
netRouter := &nbv1alpha1.NetworkRouter{
ObjectMeta: metav1.ObjectMeta{
Name: nn.Name,
Namespace: nn.Namespace,
},
Spec: nbv1alpha1.NetworkRouterSpec{
DNSZoneRef: nbv1alpha1.DNSZoneReference{
Name: "cluster.local",
},
},
}
Expect(k8sClient.Create(ctx, netRouter)).To(Succeed())
group := &nbv1alpha1.Group{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("networkrouter-%s", netRouter.Name),
Namespace: nn.Namespace,
},
}
setupKey := &nbv1alpha1.SetupKey{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("networkrouter-%s", netRouter.Name),
Namespace: nn.Namespace,
},
}
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("networkrouter-%s", netRouter.Name),
Namespace: nn.Namespace,
},
}
for range 3 {
_, err := netRouterRec.Reconcile(ctx, reconcile.Request{NamespacedName: nn})
Expect(err).NotTo(HaveOccurred())
_, err = groupRec.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(group)})
Expect(err).NotTo(HaveOccurred())
_, err = setupKeyRec.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(setupKey)})
Expect(err).NotTo(HaveOccurred())
}
err = k8sClient.Get(ctx, nn, netRouter)
Expect(err).NotTo(HaveOccurred())
Expect(netRouter.Status.NetworkID).ToNot(BeEmpty())
_, err = netRouterRec.Netbird.Networks.Routers(netRouter.Status.NetworkID).Get(ctx, netRouter.Status.RoutingPeerID)
Expect(err).NotTo(HaveOccurred())
err = k8sClient.Get(ctx, client.ObjectKeyFromObject(group), group)
Expect(err).NotTo(HaveOccurred())
Expect(group.OwnerReferences[0].UID).To(Equal(netRouter.UID))
err = k8sClient.Get(ctx, client.ObjectKeyFromObject(setupKey), setupKey)
Expect(err).NotTo(HaveOccurred())
Expect(setupKey.OwnerReferences[0].UID).To(Equal(netRouter.UID))
err = k8sClient.Get(ctx, client.ObjectKeyFromObject(dep), dep)
Expect(err).NotTo(HaveOccurred())
Expect(dep.OwnerReferences[0].UID).To(Equal(netRouter.UID))
routingPeerResp, err := netRouterRec.Netbird.Networks.Routers(netRouter.Status.NetworkID).Get(ctx, netRouter.Status.RoutingPeerID)
Expect(err).NotTo(HaveOccurred())
Expect((*routingPeerResp.PeerGroups)[0]).To(Equal(group.Status.GroupID))
})
})
})

Some files were not shown because too many files have changed in this diff Show More