diff --git a/.golangci.yml b/.golangci.yml
index ed003c99..50ad8452 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -65,6 +65,8 @@ linters:
alias: admissionv1
- pkg: k8s.io/api/policy/v1
alias: policyv1
+ - pkg: "k8s.io/api/discovery/v1"
+ alias: discoveryv1
- pkg: k8s.io/client-go/applyconfigurations/meta/v1
alias: metav1ac
- pkg: k8s.io/client-go/applyconfigurations/core/v1
@@ -75,6 +77,8 @@ linters:
alias: appsv1ac
- pkg: k8s.io/client-go/applyconfigurations/policy/v1
alias: policyv1ac
+ - pkg: k8s.io/client-go/applyconfigurations/discovery/v1
+ alias: discoveryv1ac
- pkg: sigs.k8s.io/gateway-api/apis/v1
alias: gwv1
diff --git a/PROJECT b/PROJECT
index 5564e55e..463b61b3 100644
--- a/PROJECT
+++ b/PROJECT
@@ -123,4 +123,12 @@ resources:
kind: ClusterProxy
path: github.com/netbirdio/kubernetes-operator/api/v1alpha1
version: v1alpha1
+- api:
+ crdVersion: v1
+ namespaced: true
+ controller: true
+ domain: netbird.io
+ kind: NetworkEgress
+ path: github.com/netbirdio/kubernetes-operator/api/v1alpha1
+ version: v1alpha1
version: "3"
diff --git a/README.md b/README.md
index b294f601..250768f3 100644
--- a/README.md
+++ b/README.md
@@ -23,9 +23,10 @@ helm upgrade --install --create-namespace -n netbird netbird-operator oci://ghcr
| Kind | API Version |
|------|-------------|
+| [SetupKey](docs/api-reference.md#setupkey) | `netbird.io/v1alpha1` |
| [Group](docs/api-reference.md#group) | `netbird.io/v1alpha1` |
-| [NetworkResource](docs/api-reference.md#networkresource) | `netbird.io/v1alpha1` |
| [NetworkRouter](docs/api-reference.md#networkrouter) | `netbird.io/v1alpha1` |
-| [SetupKey](docs/api-reference.md#setupkey) | `netbird.io/v1alpha1` |
+| [NetworkResource](docs/api-reference.md#networkresource) | `netbird.io/v1alpha1` |
+| [NetworkEgress](docs/api-reference.md#networkegress) | `netbird.io/v1alpha1` |
| [SidecarProfile](docs/api-reference.md#sidecarprofile) | `netbird.io/v1alpha1` |
| [ClusterProxy](docs/api-reference.md#clusterproxy) | `netbird.io/v1alpha1` |
diff --git a/api/v1alpha1/networkegress_types.go b/api/v1alpha1/networkegress_types.go
new file mode 100644
index 00000000..595159d4
--- /dev/null
+++ b/api/v1alpha1/networkegress_types.go
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// NetworkEgressSpec defines the desired state of NetworkEgress.
+type NetworkEgressSpec 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"`
+
+ // Target for egress traffic.
+ Target NetworkEgressTarget `json:"target"`
+
+ // Ports to the resource to route.
+ // +kubebuilder:validation:MinItems=1
+ // +kubebuilder:validation:Required
+ Ports []NetworkEgressPort `json:"ports"`
+}
+
+// NetworkEgressTarget describes a single allowed egress destination.
+// Exactly one of IP or FQDN must be set.
+// +kubebuilder:validation:XValidation:rule="(has(self.ip) ? 1 : 0) + (has(self.fqdn) ? 1 : 0) == 1",message="exactly one of ip or fqdn must be set"
+type NetworkEgressTarget struct {
+ // IP targets a single specific IP address (not a CIDR range).
+ // +optional
+ IP *NetworkEgressIPTarget `json:"ip,omitempty"`
+
+ // FQDN targets an exact domain name (no wildcards).
+ // +optional
+ FQDN *NetworkEgressFQDNTarget `json:"fqdn,omitempty"`
+}
+
+// NetworkEgressIPTarget is a single IPv4 or IPv6 address.
+type NetworkEgressIPTarget struct {
+ // Address is a single IP address.
+ // +kubebuilder:validation:Required
+ // +kubebuilder:validation:XValidation:rule="isIP(self)",message="address must be a valid IPv4 or IPv6 address"
+ Address string `json:"address"`
+}
+
+// NetworkEgressFQDNTarget matches traffic by an exact domain name (no wildcards).
+type NetworkEgressFQDNTarget struct {
+ // Hostname is a fully qualified domain name to match exactly.
+ // +kubebuilder:validation:Required
+ // +kubebuilder:validation:Pattern=`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`
+ Hostname string `json:"hostname"`
+}
+
+type NetworkEgressPort struct {
+ // Name of the port.
+ // +required
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=15
+ // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
+ Name string `json:"name,omitempty"`
+
+ // The port that will be exposed by this service.
+ // +required
+ // +kubebuilder:validation:Minimum=1
+ // +kubebuilder:validation:Maximum=65535
+ Port int32 `json:"port"`
+}
+
+// NetworkEgressStatus defines the observed state of NetworkEgress.
+type NetworkEgressStatus struct {
+ // ObservedGeneration is the last reconciled generation.
+ // +optional
+ ObservedGeneration int64 `json:"observedGeneration,omitempty"`
+
+ // Conditions holds the conditions for the NetworkEgress.
+ // +listType=map
+ // +listMapKey=type
+ // +optional
+ Conditions []metav1.Condition `json:"conditions,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=""
+
+// NetworkEgress is the Schema for the networkegresses API.
+type NetworkEgress struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ // +required
+ Spec NetworkEgressSpec `json:"spec"`
+
+ // +kubebuilder:default={"observedGeneration":-1}
+ Status NetworkEgressStatus `json:"status,omitempty"`
+}
+
+// GetConditions returns the status conditions of the object.
+func (n *NetworkEgress) GetConditions() []metav1.Condition {
+ return n.Status.Conditions
+}
+
+// SetConditions sets the status conditions on the object.
+func (n *NetworkEgress) SetConditions(conditions []metav1.Condition) {
+ n.Status.Conditions = conditions
+}
+
+// +kubebuilder:object:root=true
+
+// NetworkEgressList contains a list of NetworkEgress
+type NetworkEgressList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitzero"`
+ Items []NetworkEgress `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&NetworkEgress{}, &NetworkEgressList{})
+}
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index fbf0304b..2823b0d9 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -313,6 +313,179 @@ 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 *NetworkEgress) DeepCopyInto(out *NetworkEgress) {
+ *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 NetworkEgress.
+func (in *NetworkEgress) DeepCopy() *NetworkEgress {
+ if in == nil {
+ return nil
+ }
+ out := new(NetworkEgress)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NetworkEgress) 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 *NetworkEgressFQDNTarget) DeepCopyInto(out *NetworkEgressFQDNTarget) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkEgressFQDNTarget.
+func (in *NetworkEgressFQDNTarget) DeepCopy() *NetworkEgressFQDNTarget {
+ if in == nil {
+ return nil
+ }
+ out := new(NetworkEgressFQDNTarget)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NetworkEgressIPTarget) DeepCopyInto(out *NetworkEgressIPTarget) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkEgressIPTarget.
+func (in *NetworkEgressIPTarget) DeepCopy() *NetworkEgressIPTarget {
+ if in == nil {
+ return nil
+ }
+ out := new(NetworkEgressIPTarget)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NetworkEgressList) DeepCopyInto(out *NetworkEgressList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]NetworkEgress, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkEgressList.
+func (in *NetworkEgressList) DeepCopy() *NetworkEgressList {
+ if in == nil {
+ return nil
+ }
+ out := new(NetworkEgressList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NetworkEgressList) 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 *NetworkEgressPort) DeepCopyInto(out *NetworkEgressPort) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkEgressPort.
+func (in *NetworkEgressPort) DeepCopy() *NetworkEgressPort {
+ if in == nil {
+ return nil
+ }
+ out := new(NetworkEgressPort)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NetworkEgressSpec) DeepCopyInto(out *NetworkEgressSpec) {
+ *out = *in
+ out.NetworkRouterRef = in.NetworkRouterRef
+ in.Target.DeepCopyInto(&out.Target)
+ if in.Ports != nil {
+ in, out := &in.Ports, &out.Ports
+ *out = make([]NetworkEgressPort, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkEgressSpec.
+func (in *NetworkEgressSpec) DeepCopy() *NetworkEgressSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(NetworkEgressSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NetworkEgressStatus) DeepCopyInto(out *NetworkEgressStatus) {
+ *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 NetworkEgressStatus.
+func (in *NetworkEgressStatus) DeepCopy() *NetworkEgressStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(NetworkEgressStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NetworkEgressTarget) DeepCopyInto(out *NetworkEgressTarget) {
+ *out = *in
+ if in.IP != nil {
+ in, out := &in.IP, &out.IP
+ *out = new(NetworkEgressIPTarget)
+ **out = **in
+ }
+ if in.FQDN != nil {
+ in, out := &in.FQDN, &out.FQDN
+ *out = new(NetworkEgressFQDNTarget)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkEgressTarget.
+func (in *NetworkEgressTarget) DeepCopy() *NetworkEgressTarget {
+ if in == nil {
+ return nil
+ }
+ out := new(NetworkEgressTarget)
+ in.DeepCopyInto(out)
+ 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
diff --git a/charts/netbird-operator/crds/netbird.io_networkegresses.yaml b/charts/netbird-operator/crds/netbird.io_networkegresses.yaml
new file mode 100644
index 00000000..42a5350a
--- /dev/null
+++ b/charts/netbird-operator/crds/netbird.io_networkegresses.yaml
@@ -0,0 +1,200 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.20.1
+ name: networkegresses.netbird.io
+spec:
+ group: netbird.io
+ names:
+ kind: NetworkEgress
+ listKind: NetworkEgressList
+ plural: networkegresses
+ singular: networkegress
+ 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: NetworkEgress is the Schema for the networkegresses 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: NetworkEgressSpec defines the desired state of NetworkEgress.
+ properties:
+ 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
+ ports:
+ description: Ports to the resource to route.
+ items:
+ properties:
+ name:
+ description: Name of the port.
+ maxLength: 15
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ port:
+ description: The port that will be exposed by this service.
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ required:
+ - name
+ - port
+ type: object
+ minItems: 1
+ type: array
+ target:
+ description: Target for egress traffic.
+ properties:
+ fqdn:
+ description: FQDN targets an exact domain name (no wildcards).
+ properties:
+ hostname:
+ description: Hostname is a fully qualified domain name to
+ match exactly.
+ pattern: ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$
+ type: string
+ required:
+ - hostname
+ type: object
+ ip:
+ description: IP targets a single specific IP address (not a CIDR
+ range).
+ properties:
+ address:
+ description: Address is a single IP address.
+ type: string
+ x-kubernetes-validations:
+ - message: address must be a valid IPv4 or IPv6 address
+ rule: isIP(self)
+ required:
+ - address
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of ip or fqdn must be set
+ rule: '(has(self.ip) ? 1 : 0) + (has(self.fqdn) ? 1 : 0) == 1'
+ required:
+ - networkRouterRef
+ - ports
+ - target
+ type: object
+ status:
+ default:
+ observedGeneration: -1
+ description: NetworkEgressStatus defines the observed state of NetworkEgress.
+ properties:
+ conditions:
+ description: Conditions holds the conditions for the NetworkEgress.
+ 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
+ observedGeneration:
+ description: ObservedGeneration is the last reconciled generation.
+ format: int64
+ type: integer
+ type: object
+ required:
+ - spec
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/charts/netbird-operator/templates/rbac.yaml b/charts/netbird-operator/templates/rbac.yaml
index 8909c436..2b2873b9 100644
--- a/charts/netbird-operator/templates/rbac.yaml
+++ b/charts/netbird-operator/templates/rbac.yaml
@@ -38,6 +38,7 @@ rules:
- groups
- networkrouters
- networkresources
+ - networkegresses
- sidecarprofiles
- clusterproxies
verbs:
@@ -59,6 +60,7 @@ rules:
- groups/status
- networkrouters/status
- networkresources/status
+ - networkregresses/status
- sidecarprofiles/status
- clusterproxies/status
verbs:
@@ -76,19 +78,60 @@ rules:
- groups/finalizers
- networkrouters/finalizers
- networkresources/finalizers
+ - networkegresses/finalizers
- sidecarprofiles/finalizers
verbs:
- update
+- apiGroups:
+ - "discovery.k8s.io"
+ resources:
+ - endpointslices
+ verbs:
+ - get
+ - patch
+ - update
+ - list
+ - watch
+ - create
+ - delete
+- apiGroups:
+ - "rbac.authorization.k8s.io"
+ resources:
+ - roles
+ - rolebindings
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
- apiGroups:
- ""
resources:
- - services
+ - configmaps
+ - serviceaccounts
verbs:
- get
- list
- watch
+ - create
- update
- patch
+ - delete
+- apiGroups:
+ - ""
+ resources:
+ - services
+ verbs:
+ - get
+ - patch
+ - update
+ - list
+ - watch
+ - create
+ - delete
- apiGroups:
- ""
resources:
@@ -197,18 +240,6 @@ metadata:
labels:
{{- include "netbird-operator.labels" . | nindent 4 }}
rules:
-- apiGroups:
- - ""
- resources:
- - configmaps
- verbs:
- - get
- - list
- - watch
- - create
- - update
- - patch
- - delete
- apiGroups:
- coordination.k8s.io
resources:
diff --git a/cmd/main.go b/cmd/main.go
index eb57c960..5ceaeda6 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -312,6 +312,19 @@ func main() {
setupLog.Error(err, "Failed to create controller", "controller", "NetworkResource")
os.Exit(1)
}
+ if err := (&controller.NetworkEgressReconciler{
+ Client: mgr.GetClient(),
+ Netbird: nbClient,
+ }).SetupWithManager(mgr); err != nil {
+ setupLog.Error(err, "Failed to create controller", "controller", "NetworkEgress")
+ os.Exit(1)
+ }
+ if err := (&controller.ForwarderServiceReconciler{
+ Client: mgr.GetClient(),
+ }).SetupWithManager(mgr); err != nil {
+ setupLog.Error(err, "Failed to create controller", "controller", "ForwarderService")
+ os.Exit(1)
+ }
if err := (&controller.ClusterProxyReconciler{
Client: mgr.GetClient(),
ApiKey: netbirdAPIKey,
diff --git a/config/crd/bases/netbird.io_networkegresses.yaml b/config/crd/bases/netbird.io_networkegresses.yaml
new file mode 100644
index 00000000..42a5350a
--- /dev/null
+++ b/config/crd/bases/netbird.io_networkegresses.yaml
@@ -0,0 +1,200 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.20.1
+ name: networkegresses.netbird.io
+spec:
+ group: netbird.io
+ names:
+ kind: NetworkEgress
+ listKind: NetworkEgressList
+ plural: networkegresses
+ singular: networkegress
+ 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: NetworkEgress is the Schema for the networkegresses 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: NetworkEgressSpec defines the desired state of NetworkEgress.
+ properties:
+ 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
+ ports:
+ description: Ports to the resource to route.
+ items:
+ properties:
+ name:
+ description: Name of the port.
+ maxLength: 15
+ minLength: 1
+ pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+ type: string
+ port:
+ description: The port that will be exposed by this service.
+ format: int32
+ maximum: 65535
+ minimum: 1
+ type: integer
+ required:
+ - name
+ - port
+ type: object
+ minItems: 1
+ type: array
+ target:
+ description: Target for egress traffic.
+ properties:
+ fqdn:
+ description: FQDN targets an exact domain name (no wildcards).
+ properties:
+ hostname:
+ description: Hostname is a fully qualified domain name to
+ match exactly.
+ pattern: ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$
+ type: string
+ required:
+ - hostname
+ type: object
+ ip:
+ description: IP targets a single specific IP address (not a CIDR
+ range).
+ properties:
+ address:
+ description: Address is a single IP address.
+ type: string
+ x-kubernetes-validations:
+ - message: address must be a valid IPv4 or IPv6 address
+ rule: isIP(self)
+ required:
+ - address
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of ip or fqdn must be set
+ rule: '(has(self.ip) ? 1 : 0) + (has(self.fqdn) ? 1 : 0) == 1'
+ required:
+ - networkRouterRef
+ - ports
+ - target
+ type: object
+ status:
+ default:
+ observedGeneration: -1
+ description: NetworkEgressStatus defines the observed state of NetworkEgress.
+ properties:
+ conditions:
+ description: Conditions holds the conditions for the NetworkEgress.
+ 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
+ observedGeneration:
+ description: ObservedGeneration is the last reconciled generation.
+ format: int64
+ type: integer
+ type: object
+ required:
+ - spec
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 0e7f6bc3..4e95db70 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -12,4 +12,5 @@ resources:
- bases/netbird.io_setupkeys.yaml
- bases/netbird.io_sidecarprofiles.yaml
- bases/netbird.io_clusterproxies.yaml
+- bases/netbird.io_networkegresses.yaml
# +kubebuilder:scaffold:crdkustomizeresource
diff --git a/docs/api-reference.md b/docs/api-reference.md
index cd40a0de..8f765a53 100644
--- a/docs/api-reference.md
+++ b/docs/api-reference.md
@@ -11,6 +11,7 @@ Package v1alpha1 contains API Schema definitions for the v1alpha1 API group.
### Resource Types
- [ClusterProxy](#clusterproxy)
- [Group](#group)
+- [NetworkEgress](#networkegress)
- [NetworkResource](#networkresource)
- [NetworkRouter](#networkrouter)
- [SetupKey](#setupkey)
@@ -105,6 +106,7 @@ _Appears in:_
_Appears in:_
+- [NetworkEgressSpec](#networkegressspec)
- [NetworkResourceSpec](#networkresourcespec)
| Field | Description | Default | Validation |
@@ -222,6 +224,129 @@ _Appears in:_
| `Container` | InjectionModeContainer injects the client as a regular container.
|
+#### NetworkEgress
+
+
+
+NetworkEgress is the Schema for the networkegresses API.
+
+
+
+
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `apiVersion` _string_ | `netbird.io/v1alpha1` | | |
+| `kind` _string_ | `NetworkEgress` | | |
+| `kind` _string_ | 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 | | Optional: \{\}
|
+| `apiVersion` _string_ | 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 | | Optional: \{\}
|
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
+| `spec` _[NetworkEgressSpec](#networkegressspec)_ | | | Required: \{\}
|
+| `status` _[NetworkEgressStatus](#networkegressstatus)_ | | \{ observedGeneration:-1 \} | |
+
+
+#### NetworkEgressFQDNTarget
+
+
+
+NetworkEgressFQDNTarget matches traffic by an exact domain name (no wildcards).
+
+
+
+_Appears in:_
+- [NetworkEgressTarget](#networkegresstarget)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `hostname` _string_ | Hostname is a fully qualified domain name to match exactly. | | Pattern: `^([a-zA-Z0-9]([a-zA-Z0-9-]\{0,61\}[a-zA-Z0-9])?\.)+[a-zA-Z]\{2,\}$`
Required: \{\}
|
+
+
+#### NetworkEgressIPTarget
+
+
+
+NetworkEgressIPTarget is a single IPv4 or IPv6 address.
+
+
+
+_Appears in:_
+- [NetworkEgressTarget](#networkegresstarget)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `address` _string_ | Address is a single IP address. | | Required: \{\}
|
+
+
+#### NetworkEgressPort
+
+
+
+
+
+
+
+_Appears in:_
+- [NetworkEgressSpec](#networkegressspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `name` _string_ | Name of the port. | | MaxLength: 15
MinLength: 1
Pattern: `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
Required: \{\}
|
+| `port` _integer_ | The port that will be exposed by this service. | | Maximum: 65535
Minimum: 1
Required: \{\}
|
+
+
+#### NetworkEgressSpec
+
+
+
+NetworkEgressSpec defines the desired state of NetworkEgress.
+
+
+
+_Appears in:_
+- [NetworkEgress](#networkegress)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `networkRouterRef` _[CrossNamespaceReference](#crossnamespacereference)_ | NetworkRouterRef is a reference to the network and router where the resource will be created. | | |
+| `target` _[NetworkEgressTarget](#networkegresstarget)_ | Target for egress traffic. | | |
+| `ports` _[NetworkEgressPort](#networkegressport) array_ | Ports to the resource to route. | | MinItems: 1
Required: \{\}
|
+
+
+#### NetworkEgressStatus
+
+
+
+NetworkEgressStatus defines the observed state of NetworkEgress.
+
+
+
+_Appears in:_
+- [NetworkEgress](#networkegress)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `observedGeneration` _integer_ | ObservedGeneration is the last reconciled generation. | | Optional: \{\}
|
+| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#condition-v1-meta) array_ | Conditions holds the conditions for the NetworkEgress. | | Optional: \{\}
|
+
+
+#### NetworkEgressTarget
+
+
+
+NetworkEgressTarget describes a single allowed egress destination.
+Exactly one of IP or FQDN must be set.
+
+
+
+_Appears in:_
+- [NetworkEgressSpec](#networkegressspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `ip` _[NetworkEgressIPTarget](#networkegressiptarget)_ | IP targets a single specific IP address (not a CIDR range). | | Optional: \{\}
|
+| `fqdn` _[NetworkEgressFQDNTarget](#networkegressfqdntarget)_ | FQDN targets an exact domain name (no wildcards). | | Optional: \{\}
|
+
+
#### NetworkResource
diff --git a/examples/network/networkegress.yaml b/examples/network/networkegress.yaml
new file mode 100644
index 00000000..f5a9b08d
--- /dev/null
+++ b/examples/network/networkegress.yaml
@@ -0,0 +1,15 @@
+apiVersion: netbird.io/v1alpha1
+kind: NetworkEgress
+metadata:
+ name: nginx
+ namespace: default
+spec:
+ networkRouterRef:
+ name: prod
+ namespace: netbird
+ target:
+ ip:
+ address: 10.96.92.40
+ ports:
+ - name: http
+ port: 80
diff --git a/go.mod b/go.mod
index 6a183d69..917f6328 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
github.com/go-logr/logr v1.4.3
github.com/go-openapi/testify/v2 v2.6.0
github.com/google/uuid v1.6.0
+ github.com/netbirdio/kube-egress-forwarder v0.0.2
github.com/netbirdio/netbird v0.72.4
github.com/onsi/ginkgo/v2 v2.32.0
github.com/onsi/gomega v1.42.1
@@ -79,7 +80,7 @@ require (
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
- golang.org/x/sync v0.21.0 // indirect
+ golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
golang.org/x/text v0.38.0 // indirect
diff --git a/go.sum b/go.sum
index 83e4b126..15f71acc 100644
--- a/go.sum
+++ b/go.sum
@@ -340,6 +340,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/netbirdio/dex v0.244.1-0.20260512110716-8d70ad8647c1 h1:4TaYr9O4xX0D2kszeOLclTiCbA3eHq3xWV+9ILJbIYs=
github.com/netbirdio/dex v0.244.1-0.20260512110716-8d70ad8647c1/go.mod h1:IHH+H8vK2GfqtIt5u/5OdPh18yk0oDHuj2vz5+Goetg=
+github.com/netbirdio/kube-egress-forwarder v0.0.2 h1:jUlgA8lSDKyqn9tUdRFtEadXdDCFA63ZFPenQrMGv7E=
+github.com/netbirdio/kube-egress-forwarder v0.0.2/go.mod h1:Lyt9k/93DTWaeOiGQWM63WtQNj/9NnMTR/PivDgWXXE=
github.com/netbirdio/management-integrations/integrations v0.0.0-20260416123949-2355d972be42 h1:F3zS5fT9xzD1OFLfcdAE+3FfyiwjGukF1hvj0jErgs8=
github.com/netbirdio/management-integrations/integrations v0.0.0-20260416123949-2355d972be42/go.mod h1:n47r67ZSPgwSmT/Z1o48JjZQW9YJ6m/6Bd/uAXkL3Pg=
github.com/netbirdio/netbird v0.72.4 h1:Pjs5PZqIvjJ4gqTW+RNh2sBMjsgOFd1Rf0SDoYsHqLM=
@@ -509,8 +511,8 @@ golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
-golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
-golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
+golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
diff --git a/internal/controller/forwarderservice_controller.go b/internal/controller/forwarderservice_controller.go
new file mode 100644
index 00000000..539dbbe5
--- /dev/null
+++ b/internal/controller/forwarderservice_controller.go
@@ -0,0 +1,344 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+package controller
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "maps"
+ "net/netip"
+ "strings"
+ "time"
+
+ corev1 "k8s.io/api/core/v1"
+ discoveryv1 "k8s.io/api/discovery/v1"
+ kerrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/apimachinery/pkg/selection"
+ "k8s.io/apimachinery/pkg/types"
+ corev1ac "k8s.io/client-go/applyconfigurations/core/v1"
+ discoveryv1ac "k8s.io/client-go/applyconfigurations/discovery/v1"
+ 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/handler"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+ "github.com/netbirdio/kube-egress-forwarder/pkg/forwarder"
+
+ nbv1alpha1 "github.com/netbirdio/kubernetes-operator/api/v1alpha1"
+ "github.com/netbirdio/kubernetes-operator/internal/k8sutil"
+)
+
+const (
+ ForwarderRouterNameLabel = "netbird.io/forwarder-router-name"
+ EgressRouterNameLabel = "netbird.io/egress-router-name"
+ EgressRouterNamespaceLabel = "netbird.io/egress-router-namespace"
+ LastUpdatedLabel = "netbird.io/last-updated"
+)
+
+// ForwarderServiceReconciler reconciles a EndpointSlice object
+type ForwarderServiceReconciler struct {
+ client.Client
+}
+
+func (r *ForwarderServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ svc := &corev1.Service{}
+ err := r.Get(ctx, req.NamespacedName, svc)
+ if err != nil {
+ return ctrl.Result{}, client.IgnoreNotFound(err)
+ }
+ if !svc.DeletionTimestamp.IsZero() {
+ return ctrl.Result{}, nil
+ }
+
+ ownerRef, err := k8sutil.ControllerReference(svc, r.Scheme())
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ routerName, ok := svc.Labels[ForwarderRouterNameLabel]
+ if !ok {
+ return ctrl.Result{}, errors.New("missing forwarder router label")
+ }
+
+ // Load port manager state.
+ ruleConfigmap := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: req.Name,
+ Namespace: req.Namespace,
+ },
+ }
+ err = r.Client.Get(ctx, client.ObjectKeyFromObject(ruleConfigmap), ruleConfigmap)
+ if err != nil && !kerrors.IsNotFound(err) {
+ return ctrl.Result{}, err
+ }
+ ruleMgr, err := forwarder.NewRuleManager(ruleConfigmap.Data)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ // Get endpoint slices for forwarder service.
+ endpointSliceList := &discoveryv1.EndpointSliceList{}
+ err = r.Client.List(ctx, endpointSliceList, &client.MatchingLabels{discoveryv1.LabelServiceName: svc.Name})
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ // Get services for egress resources.
+ egressSvcList := &corev1.ServiceList{}
+ err = r.Client.List(ctx, egressSvcList, &client.MatchingLabels{EgressRouterNameLabel: routerName, EgressRouterNamespaceLabel: req.Namespace})
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ // Copy router endpoint slices to egress services.
+ lastUpdated := fmt.Sprintf("%d", time.Now().Unix())
+
+ for _, egressSvc := range egressSvcList.Items {
+ netEgress := &nbv1alpha1.NetworkEgress{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: egressSvc.OwnerReferences[0].Name,
+ Namespace: egressSvc.Namespace,
+ },
+ }
+ err = r.Get(ctx, client.ObjectKeyFromObject(netEgress), netEgress)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ targetPorts := []int32{}
+ for _, port := range egressSvc.Spec.Ports {
+ dest, err := func() (string, error) {
+ switch {
+ case netEgress.Spec.Target.IP != nil:
+ addr, err := netip.ParseAddr(netEgress.Spec.Target.IP.Address)
+ if err != nil {
+ return "", err
+ }
+ return netip.AddrPortFrom(addr, uint16(port.Port)).String(), nil
+ case netEgress.Spec.Target.FQDN != nil:
+ return fmt.Sprintf("%s:%d", netEgress.Spec.Target.FQDN.Hostname, port.Port), nil
+ }
+ return "", errors.New("egress target not found")
+ }()
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ rule := ruleMgr.Allocate(port.Protocol, dest)
+ targetPorts = append(targetPorts, rule.Port)
+ }
+ portACs := toPortApplyConfigurations(egressSvc.Spec.Ports, targetPorts)
+
+ egressSvcOwnerRef, err := k8sutil.ControllerReference(&egressSvc, r.Scheme())
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ for _, endpointSlice := range endpointSliceList.Items {
+ nameSuffx := strings.TrimPrefix(endpointSlice.Name, endpointSlice.GenerateName)
+
+ labels := map[string]string{
+ discoveryv1.LabelServiceName: egressSvc.Name,
+ discoveryv1.LabelManagedBy: "netbird-operator.netbird.io",
+ LastUpdatedLabel: lastUpdated,
+ }
+ maps.Copy(labels, egressSvc.Labels)
+
+ endpointACs := toEndpointApplyConfigurations(endpointSlice.Endpoints)
+ endpointSliceAC := discoveryv1ac.EndpointSlice(fmt.Sprintf("%s-%s", egressSvc.Name, nameSuffx), egressSvc.Namespace).
+ WithLabels(labels).
+ WithOwnerReferences(egressSvcOwnerRef).
+ WithAddressType(endpointSlice.AddressType).
+ WithEndpoints(endpointACs...).
+ WithPorts(portACs...)
+ err = r.Client.Apply(ctx, endpointSliceAC, client.ForceOwnership)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ }
+ }
+
+ // Write the port config to the port conversion.
+ data, err := ruleMgr.Data()
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ cmAC := corev1ac.ConfigMap(req.Name, req.Namespace).
+ WithOwnerReferences(ownerRef).
+ WithData(data)
+ err = r.Client.Apply(ctx, cmAC, client.ForceOwnership)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ // Cleanup old endpoint slices.
+ updateReq, err := labels.NewRequirement(LastUpdatedLabel, selection.NotEquals, []string{lastUpdated})
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ egressNameReq, err := labels.NewRequirement(EgressRouterNameLabel, selection.Equals, []string{routerName})
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ egressNamespaceReq, err := labels.NewRequirement(EgressRouterNamespaceLabel, selection.Equals, []string{req.Namespace})
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ sourceReq, err := labels.NewRequirement(discoveryv1.LabelServiceName, selection.NotEquals, []string{svc.Name})
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ deleteSelector := labels.NewSelector().Add(*updateReq).Add(*egressNameReq).Add(*egressNamespaceReq).Add(*sourceReq)
+
+ err = r.Client.List(ctx, endpointSliceList, client.MatchingLabelsSelector{Selector: deleteSelector})
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ for _, item := range endpointSliceList.Items {
+ if err := r.Client.Delete(ctx, &item); err != nil && !kerrors.IsNotFound(err) {
+ return ctrl.Result{}, err
+ }
+ }
+
+ return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *ForwarderServiceReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ forwarderSvcSelector := metav1.LabelSelector{
+ MatchExpressions: []metav1.LabelSelectorRequirement{
+ {
+ Key: ForwarderRouterNameLabel,
+ Operator: metav1.LabelSelectorOpExists,
+ },
+ },
+ }
+ forwarderSvcPred, err := predicate.LabelSelectorPredicate(forwarderSvcSelector)
+ if err != nil {
+ return err
+ }
+
+ egressSvcSelector := metav1.LabelSelector{
+ MatchExpressions: []metav1.LabelSelectorRequirement{
+ {
+ Key: EgressRouterNameLabel,
+ Operator: metav1.LabelSelectorOpExists,
+ },
+ {
+ Key: EgressRouterNamespaceLabel,
+ Operator: metav1.LabelSelectorOpExists,
+ },
+ },
+ }
+ egressSvcPred, err := predicate.LabelSelectorPredicate(egressSvcSelector)
+ if err != nil {
+ return err
+ }
+
+ return ctrl.NewControllerManagedBy(mgr).
+ Named("forwarderservice").
+ For(&corev1.Service{}, builder.WithPredicates(forwarderSvcPred)).
+ Owns(&discoveryv1.EndpointSlice{}, builder.WithPredicates(forwarderSvcPred)).
+ Owns(&corev1.ConfigMap{}).
+ Watches(&nbv1alpha1.NetworkEgress{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
+ imp, ok := obj.(*nbv1alpha1.NetworkEgress)
+ if !ok {
+ return nil
+ }
+ return []reconcile.Request{
+ {
+ NamespacedName: types.NamespacedName{
+ Name: fmt.Sprintf("networkrouter-%s-forwarder", imp.Spec.NetworkRouterRef.Name),
+ Namespace: imp.Spec.NetworkRouterRef.Namespace,
+ },
+ },
+ }
+ })).
+ Watches(&corev1.Service{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
+ return []reconcile.Request{
+ {
+ NamespacedName: types.NamespacedName{
+ Name: fmt.Sprintf("networkrouter-%s-forwarder", obj.GetLabels()[EgressRouterNameLabel]),
+ Namespace: obj.GetLabels()[EgressRouterNamespaceLabel],
+ },
+ },
+ }
+ }), builder.WithPredicates(egressSvcPred)).
+ Watches(&discoveryv1.EndpointSlice{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
+ return []reconcile.Request{
+ {
+ NamespacedName: types.NamespacedName{
+ Name: fmt.Sprintf("networkrouter-%s-forwarder", obj.GetLabels()[EgressRouterNameLabel]),
+ Namespace: obj.GetLabels()[EgressRouterNamespaceLabel],
+ },
+ },
+ }
+ }), builder.WithPredicates(egressSvcPred)).
+ Complete(r)
+}
+
+func toEndpointApplyConfigurations(endpoints []discoveryv1.Endpoint) []*discoveryv1ac.EndpointApplyConfiguration {
+ endpointACs := make([]*discoveryv1ac.EndpointApplyConfiguration, 0, len(endpoints))
+ for _, endpoint := range endpoints {
+ conditionAC := discoveryv1ac.EndpointConditions()
+ if endpoint.Conditions.Ready != nil {
+ conditionAC.WithReady(*endpoint.Conditions.Ready)
+ }
+ if endpoint.Conditions.Serving != nil {
+ conditionAC.WithServing(*endpoint.Conditions.Serving)
+ }
+ if endpoint.Conditions.Terminating != nil {
+ conditionAC.WithTerminating(*endpoint.Conditions.Terminating)
+ }
+
+ endpointAC := discoveryv1ac.Endpoint().
+ WithAddresses(endpoint.Addresses...).
+ WithConditions(conditionAC)
+
+ if endpoint.NodeName != nil {
+ endpointAC = endpointAC.WithNodeName(*endpoint.NodeName)
+ }
+ if endpoint.Zone != nil {
+ endpointAC = endpointAC.WithZone(*endpoint.Zone)
+ }
+ if endpoint.Hints != nil {
+ hintAC := discoveryv1ac.EndpointHints()
+ for _, hint := range endpoint.Hints.ForNodes {
+ hintAC = hintAC.WithForNodes(discoveryv1ac.ForNode().WithName(hint.Name))
+ }
+ for _, hint := range endpoint.Hints.ForZones {
+ hintAC = hintAC.WithForZones(discoveryv1ac.ForZone().WithName(hint.Name))
+ }
+ endpointAC = endpointAC.WithHints(hintAC)
+ }
+ if endpoint.TargetRef != nil {
+ endpointAC = endpointAC.WithTargetRef(corev1ac.ObjectReference().
+ WithResourceVersion(endpoint.TargetRef.ResourceVersion).
+ WithFieldPath(endpoint.TargetRef.FieldPath).
+ WithUID(endpoint.TargetRef.UID).
+ WithAPIVersion(endpoint.TargetRef.APIVersion).
+ WithKind(endpoint.TargetRef.Kind).
+ WithName(endpoint.TargetRef.Name).
+ WithNamespace(endpoint.TargetRef.Namespace),
+ )
+ }
+
+ endpointACs = append(endpointACs, endpointAC)
+ }
+ return endpointACs
+}
+
+func toPortApplyConfigurations(ports []corev1.ServicePort, targetPorts []int32) []*discoveryv1ac.EndpointPortApplyConfiguration {
+ portACs := make([]*discoveryv1ac.EndpointPortApplyConfiguration, 0, len(ports))
+ for i, port := range ports {
+ portAC := discoveryv1ac.EndpointPort().WithName(port.Name).WithPort(targetPorts[i]).WithProtocol(port.Protocol)
+ if port.AppProtocol != nil {
+ portAC = portAC.WithAppProtocol(*port.AppProtocol)
+ }
+ portACs = append(portACs, portAC)
+ }
+ return portACs
+}
diff --git a/internal/controller/networkegress_controller.go b/internal/controller/networkegress_controller.go
new file mode 100644
index 00000000..e56551f3
--- /dev/null
+++ b/internal/controller/networkegress_controller.go
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+package controller
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/fluxcd/pkg/runtime/conditions"
+ "github.com/fluxcd/pkg/runtime/patch"
+ 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"
+ corev1ac "k8s.io/client-go/applyconfigurations/core/v1"
+ 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/handler"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+ netbird "github.com/netbirdio/netbird/shared/management/client/rest"
+
+ nbv1alpha1 "github.com/netbirdio/kubernetes-operator/api/v1alpha1"
+ "github.com/netbirdio/kubernetes-operator/internal/k8sutil"
+)
+
+// NetworkEgressReconciler reconciles a NetworkEgress object
+type NetworkEgressReconciler struct {
+ client.Client
+
+ Netbird *netbird.Client
+}
+
+// +kubebuilder:rbac:groups=netbird.io,resources=networkegresses,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=netbird.io,resources=networkegresses/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=netbird.io,resources=networkegresses/finalizers,verbs=update
+
+func (r *NetworkEgressReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ netEgress := &nbv1alpha1.NetworkEgress{}
+ err := r.Get(ctx, req.NamespacedName, netEgress)
+ if err != nil {
+ return ctrl.Result{}, client.IgnoreNotFound(err)
+ }
+ sp := patch.NewSerialPatcher(netEgress, r.Client)
+
+ if !netEgress.DeletionTimestamp.IsZero() {
+ return ctrl.Result{}, nil
+ }
+
+ ownerRef, err := k8sutil.ControllerReference(netEgress, r.Scheme())
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ netRouter := &nbv1alpha1.NetworkRouter{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: netEgress.Spec.NetworkRouterRef.Name,
+ Namespace: netEgress.Spec.NetworkRouterRef.Namespace,
+ },
+ }
+ err = r.Get(ctx, client.ObjectKeyFromObject(netRouter), netRouter)
+ if err != nil {
+ if kerrors.IsNotFound(err) {
+ conditions.MarkFalse(netEgress, nbv1alpha1.ReadyCondition, nbv1alpha1.DependencyReason, "Referenced NetworkRouter cannot be found.")
+ err = sp.Patch(ctx, netEgress)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ return ctrl.Result{}, nil
+ }
+ return ctrl.Result{}, err
+ }
+
+ // Create service for egress resource to do port remapping.
+ ports := []*corev1ac.ServicePortApplyConfiguration{}
+ for _, port := range netEgress.Spec.Ports {
+ ports = append(ports, corev1ac.ServicePort().WithName(port.Name).WithPort(port.Port))
+ }
+ routerSvcAC := corev1ac.Service(netEgress.Name, netEgress.Namespace).
+ WithLabels(map[string]string{EgressRouterNameLabel: netRouter.Name, EgressRouterNamespaceLabel: netRouter.Namespace}).
+ WithOwnerReferences(ownerRef).
+ WithSpec(
+ corev1ac.ServiceSpec().
+ WithPorts(ports...),
+ )
+ err = r.Client.Apply(ctx, routerSvcAC, client.ForceOwnership)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ conditions.MarkTrue(netEgress, nbv1alpha1.ReadyCondition, nbv1alpha1.ReconciledReason, "")
+ err = sp.Patch(ctx, netEgress, patch.WithStatusObservedGeneration{})
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *NetworkEgressReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ err := mgr.GetFieldIndexer().IndexField(context.Background(), &nbv1alpha1.NetworkEgress{}, ".spec.networkRouterRef", func(obj client.Object) []string {
+ netEgress := obj.(*nbv1alpha1.NetworkEgress)
+ ref := netEgress.Spec.NetworkRouterRef
+ if ref.Name == "" {
+ return nil
+ }
+ if ref.Namespace == "" {
+ ref.Namespace = netEgress.Namespace
+ }
+ return []string{fmt.Sprintf("%s/%s", ref.Name, ref.Namespace)}
+ })
+ if err != nil {
+ return err
+ }
+
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&nbv1alpha1.NetworkEgress{}).
+ Owns(&corev1.Service{}).
+ Watches(
+ &nbv1alpha1.NetworkRouter{},
+ handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
+ netEgressList := &nbv1alpha1.NetworkEgressList{}
+ err := r.List(ctx, netEgressList, client.MatchingFields{".spec.networkRouterRef": fmt.Sprintf("%s/%s", obj.GetName(), obj.GetNamespace())})
+ if err != nil {
+ return nil
+ }
+
+ requests := make([]reconcile.Request, len(netEgressList.Items))
+ for i, item := range netEgressList.Items {
+ requests[i] = reconcile.Request{
+ NamespacedName: types.NamespacedName{
+ Name: item.Name,
+ Namespace: item.Namespace,
+ },
+ }
+ }
+ return requests
+ }),
+ builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
+ ).
+ Complete(r)
+}
diff --git a/internal/controller/networkegress_controller_test.go b/internal/controller/networkegress_controller_test.go
new file mode 100644
index 00000000..d18e7e46
--- /dev/null
+++ b/internal/controller/networkegress_controller_test.go
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+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"
+)
+
+var _ = Describe("NetworkEgress Controller", func() {
+ Context("When reconciling a resource", func() {
+ ctx := context.Background()
+
+ var netEgressRec *NetworkEgressReconciler
+ var forwarderRec *ForwarderServiceReconciler
+
+ nn := client.ObjectKey{
+ Name: "test-resource",
+ Namespace: "network-egress",
+ }
+ BeforeEach(func() {
+ netEgressRec = &NetworkEgressReconciler{
+ Client: k8sClient,
+ }
+ forwarderRec = &ForwarderServiceReconciler{
+ Client: k8sClient,
+ }
+
+ 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 egress service with endpoint slices", func() {
+ netRouter := &nbv1alpha1.NetworkRouter{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "egress-router",
+ Namespace: nn.Namespace,
+ },
+ Spec: nbv1alpha1.NetworkRouterSpec{
+ DNSZoneRef: nbv1alpha1.DNSZoneReference{
+ Name: "foo.bar",
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, netRouter)).To(Succeed())
+
+ netEgress := &nbv1alpha1.NetworkEgress{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: nn.Name,
+ Namespace: nn.Namespace,
+ },
+ Spec: nbv1alpha1.NetworkEgressSpec{
+ NetworkRouterRef: nbv1alpha1.CrossNamespaceReference{
+ Name: netRouter.Name,
+ Namespace: nn.Namespace,
+ },
+ Target: nbv1alpha1.NetworkEgressTarget{
+ FQDN: &nbv1alpha1.NetworkEgressFQDNTarget{
+ Hostname: "example.com",
+ },
+ },
+ Ports: []nbv1alpha1.NetworkEgressPort{
+ {
+ Name: "http",
+ Port: 80,
+ },
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, netEgress)).To(Succeed())
+ _, err := netEgressRec.Reconcile(ctx, reconcile.Request{NamespacedName: nn})
+ Expect(err).NotTo(HaveOccurred())
+
+ egressSvc := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: nn.Name,
+ Namespace: nn.Namespace,
+ },
+ }
+ err = k8sClient.Get(ctx, client.ObjectKeyFromObject(egressSvc), egressSvc)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(egressSvc.Labels[EgressRouterNameLabel]).To(Equal(netRouter.Name))
+ Expect(egressSvc.Labels[EgressRouterNamespaceLabel]).To(Equal(netRouter.Namespace))
+ Expect(egressSvc.Spec.Ports).To(HaveLen(1))
+ Expect(egressSvc.Spec.Ports[0].TargetPort.String()).To(Equal("80"))
+
+ _, err = forwarderRec.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKey{Name: fmt.Sprintf("networkrouter-%s-forwarder", netRouter.Name), Namespace: nn.Namespace}})
+ Expect(err).NotTo(HaveOccurred())
+ })
+ })
+})
diff --git a/internal/controller/networkrouter_controller.go b/internal/controller/networkrouter_controller.go
index 2d630d79..42c38cf4 100644
--- a/internal/controller/networkrouter_controller.go
+++ b/internal/controller/networkrouter_controller.go
@@ -15,6 +15,7 @@ import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
policyv1 "k8s.io/api/policy/v1"
+ rbacv1 "k8s.io/api/rbac/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -24,6 +25,7 @@ import (
corev1ac "k8s.io/client-go/applyconfigurations/core/v1"
metav1ac "k8s.io/client-go/applyconfigurations/meta/v1"
policyv1ac "k8s.io/client-go/applyconfigurations/policy/v1"
+ rbacv1ac "k8s.io/client-go/applyconfigurations/rbac/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -35,6 +37,7 @@ import (
"github.com/netbirdio/kubernetes-operator/internal/k8sutil"
"github.com/netbirdio/kubernetes-operator/internal/netbirdutil"
nbv1alpha1ac "github.com/netbirdio/kubernetes-operator/pkg/applyconfigurations/api/v1alpha1"
+ "github.com/netbirdio/kubernetes-operator/pkg/version"
)
type NetworkRouterReconciler struct {
@@ -191,7 +194,7 @@ func (r *NetworkRouterReconciler) Reconcile(ctx context.Context, req ctrl.Reques
return ctrl.Result{}, err
}
- // Create the deployment.
+ // Setup router configuration.
selectorLabels := map[string]string{
"app.kubernetes.io/name": "networkrouter",
"app.kubernetes.io/instance": req.Name,
@@ -207,6 +210,44 @@ func (r *NetworkRouterReconciler) Reconcile(ctx context.Context, req ctrl.Reques
clientImage = netRouter.Spec.Image
}
+ // Create forwarder resources.
+ forwarderName := fmt.Sprintf("networkrouter-%s-forwarder", req.Name)
+ forwarderSvcAC := corev1ac.Service(forwarderName, req.Namespace).
+ WithLabels(map[string]string{ForwarderRouterNameLabel: netRouter.Name}).
+ WithOwnerReferences(ownerRef).
+ WithSpec(
+ corev1ac.ServiceSpec().
+ WithClusterIP("None").
+ WithSelector(selectorLabels),
+ )
+ err = r.Client.Apply(ctx, forwarderSvcAC, client.ForceOwnership)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ saAC := corev1ac.ServiceAccount(forwarderName, req.Namespace).
+ WithOwnerReferences(ownerRef)
+ err = r.Client.Apply(ctx, saAC, client.ForceOwnership)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ roleAC := rbacv1ac.Role(forwarderName, req.Namespace).
+ WithOwnerReferences(ownerRef).
+ WithRules(rbacv1ac.PolicyRule().WithAPIGroups("").WithResources("configmaps").WithVerbs("get", "watch"))
+ err = r.Client.Apply(ctx, roleAC, client.ForceOwnership)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+ roleBindAC := rbacv1ac.RoleBinding(forwarderName, req.Namespace).
+ WithOwnerReferences(ownerRef).
+ WithSubjects(rbacv1ac.Subject().WithKind(*saAC.Kind).WithName(*saAC.Name)).
+ WithRoleRef(rbacv1ac.RoleRef().WithKind(*roleAC.Kind).WithName(*roleAC.Name))
+ err = r.Client.Apply(ctx, roleBindAC, client.ForceOwnership)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ // Create the deployment.
podTemplateSpecAC := corev1ac.PodTemplateSpec().
WithLabels(selectorLabels).
WithSpec(corev1ac.PodSpec().
@@ -232,65 +273,84 @@ func (r *NetworkRouterReconciler) Reconcile(ctx context.Context, req ctrl.Reques
WithReadOnlyRootFilesystem(true),
),
).
- WithContainers(corev1ac.Container().
- WithName("netbird").
- WithImage(clientImage).
- WithEnv(
- corev1ac.EnvVar().
- WithName("NB_SETUP_KEY").
- WithValueFrom(corev1ac.EnvVarSource().
- WithSecretKeyRef(corev1ac.SecretKeySelector().
- WithName(setupKey.SecretName()).
- WithKey(SetupKeySecretKey),
+ WithContainers(
+ corev1ac.Container().
+ WithName("netbird").
+ WithImage(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(logLevel),
- corev1ac.EnvVar().
- WithName("NB_LOG_FILE").
- WithValue("console"),
- corev1ac.EnvVar().
- WithName("NB_DISABLE_PROFILES").
- WithValue("true"),
- corev1ac.EnvVar().
- WithName("NB_DISABLE_UPDATE_SETTINGS").
- WithValue("true"),
- corev1ac.EnvVar().
- WithName("NB_DAEMON_ADDR").
- WithValue("unix:///var/run/netbird/netbird.sock"),
- corev1ac.EnvVar().
- WithName("NB_ENTRYPOINT_SERVICE_TIMEOUT").
- WithValue("0"),
- ).
- WithStartupProbe(corev1ac.Probe().WithExec(corev1ac.ExecAction().WithCommand("netbird", "status", "--check", "startup"))).
- WithReadinessProbe(corev1ac.Probe().WithExec(corev1ac.ExecAction().WithCommand("netbird", "status", "--check", "ready"))).
- WithVolumeMounts(
- corev1ac.VolumeMount().WithName("netbird-run").WithMountPath("/var/run/netbird"),
- corev1ac.VolumeMount().WithName("netbird-lib").WithMountPath("/var/lib/netbird"),
- corev1ac.VolumeMount().WithName("ssh-etc").WithMountPath("/etc/ssh"),
- corev1ac.VolumeMount().WithName("resolv-conf").WithMountPath("/etc/resolv.conf").WithSubPath("resolv.conf"),
- corev1ac.VolumeMount().WithName("resolv-conf").WithMountPath("/etc/resolv.conf.original.netbird").WithSubPath("resolv.conf.original.netbird"),
- ).
- WithSecurityContext(corev1ac.SecurityContext().
- WithReadOnlyRootFilesystem(true).
- WithCapabilities(corev1ac.Capabilities().
- WithAdd("NET_ADMIN").
- WithAdd("SYS_RESOURCE").
- WithAdd("SYS_ADMIN"),
+ corev1ac.EnvVar().
+ WithName("NB_MANAGEMENT_URL").
+ WithValue(r.ManagementURL),
+ corev1ac.EnvVar().
+ WithName("NB_LOG_LEVEL").
+ WithValue(logLevel),
+ corev1ac.EnvVar().
+ WithName("NB_LOG_FILE").
+ WithValue("console"),
+ corev1ac.EnvVar().
+ WithName("NB_DISABLE_PROFILES").
+ WithValue("true"),
+ corev1ac.EnvVar().
+ WithName("NB_DISABLE_UPDATE_SETTINGS").
+ WithValue("true"),
+ corev1ac.EnvVar().
+ WithName("NB_DAEMON_ADDR").
+ WithValue("unix:///var/run/netbird/netbird.sock"),
+ corev1ac.EnvVar().
+ WithName("NB_ENTRYPOINT_SERVICE_TIMEOUT").
+ WithValue("0"),
).
- WithPrivileged(true),
- ).
- WithResources(corev1ac.ResourceRequirements().
- WithRequests(corev1.ResourceList{
- corev1.ResourceCPU: resource.MustParse("100m"),
- corev1.ResourceMemory: resource.MustParse("128Mi"),
- }),
- ),
+ WithStartupProbe(corev1ac.Probe().WithExec(corev1ac.ExecAction().WithCommand("netbird", "status", "--check", "startup"))).
+ WithReadinessProbe(corev1ac.Probe().WithExec(corev1ac.ExecAction().WithCommand("netbird", "status", "--check", "ready"))).
+ WithVolumeMounts(
+ corev1ac.VolumeMount().WithName("netbird-run").WithMountPath("/var/run/netbird"),
+ corev1ac.VolumeMount().WithName("netbird-lib").WithMountPath("/var/lib/netbird"),
+ corev1ac.VolumeMount().WithName("ssh-etc").WithMountPath("/etc/ssh"),
+ corev1ac.VolumeMount().WithName("resolv-conf").WithMountPath("/etc/resolv.conf").WithSubPath("resolv.conf"),
+ corev1ac.VolumeMount().WithName("resolv-conf").WithMountPath("/etc/resolv.conf.original.netbird").WithSubPath("resolv.conf.original.netbird"),
+ ).
+ WithSecurityContext(corev1ac.SecurityContext().
+ WithReadOnlyRootFilesystem(true).
+ WithCapabilities(corev1ac.Capabilities().
+ WithAdd("NET_ADMIN").
+ WithAdd("SYS_RESOURCE").
+ WithAdd("SYS_ADMIN"),
+ ).
+ WithPrivileged(true),
+ ).
+ WithResources(corev1ac.ResourceRequirements().
+ WithRequests(corev1.ResourceList{
+ corev1.ResourceCPU: resource.MustParse("100m"),
+ corev1.ResourceMemory: resource.MustParse("128Mi"),
+ }),
+ ),
+ corev1ac.Container().
+ WithName("kube-egress-forwarder").
+ WithImage(version.KubeEgressForwarderImage).
+ WithArgs("--configmap-name", forwarderName, "--configmap-namespace", req.Namespace).
+ WithVolumeMounts(
+ corev1ac.VolumeMount().WithName("resolv-conf").WithMountPath("/etc/resolv.conf").WithSubPath("resolv.conf"),
+ ).
+ WithSecurityContext(corev1ac.SecurityContext().
+ WithCapabilities(corev1ac.Capabilities().WithDrop("ALL")).
+ WithReadOnlyRootFilesystem(true),
+ ).
+ WithResources(corev1ac.ResourceRequirements().
+ WithRequests(corev1.ResourceList{
+ corev1.ResourceCPU: resource.MustParse("100m"),
+ corev1.ResourceMemory: resource.MustParse("128Mi"),
+ }),
+ ),
).
+ WithServiceAccountName(*saAC.Name).
WithVolumes(
corev1ac.Volume().WithName("netbird-run").WithEmptyDir(corev1ac.EmptyDirVolumeSource()),
corev1ac.Volume().WithName("netbird-lib").WithEmptyDir(corev1ac.EmptyDirVolumeSource()),
@@ -421,5 +481,9 @@ func (r *NetworkRouterReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&nbv1alpha1.Group{}).
Owns(&nbv1alpha1.SetupKey{}).
Owns(&appsv1.Deployment{}).
+ Owns(&corev1.Service{}).
+ Owns(&corev1.ServiceAccount{}).
+ Owns(&rbacv1.Role{}).
+ Owns(&rbacv1.RoleBinding{}).
Complete(r)
}
diff --git a/pkg/applyconfigurations/api/v1alpha1/fqdntarget.go b/pkg/applyconfigurations/api/v1alpha1/fqdntarget.go
new file mode 100644
index 00000000..ed195588
--- /dev/null
+++ b/pkg/applyconfigurations/api/v1alpha1/fqdntarget.go
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// FQDNTargetApplyConfiguration represents a declarative configuration of the FQDNTarget type for use
+// with apply.
+//
+// FQDNTarget matches traffic by an exact domain name (no wildcards).
+type FQDNTargetApplyConfiguration struct {
+ // Hostname is a fully qualified domain name to match exactly.
+ Hostname *string `json:"hostname,omitempty"`
+}
+
+// FQDNTargetApplyConfiguration constructs a declarative configuration of the FQDNTarget type for use with
+// apply.
+func FQDNTarget() *FQDNTargetApplyConfiguration {
+ return &FQDNTargetApplyConfiguration{}
+}
+
+// WithHostname sets the Hostname field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Hostname field is set to the value of the last call.
+func (b *FQDNTargetApplyConfiguration) WithHostname(value string) *FQDNTargetApplyConfiguration {
+ b.Hostname = &value
+ return b
+}
diff --git a/pkg/applyconfigurations/api/v1alpha1/ipaddresstarget.go b/pkg/applyconfigurations/api/v1alpha1/ipaddresstarget.go
new file mode 100644
index 00000000..78409dbe
--- /dev/null
+++ b/pkg/applyconfigurations/api/v1alpha1/ipaddresstarget.go
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// IPAddressTargetApplyConfiguration represents a declarative configuration of the IPAddressTarget type for use
+// with apply.
+//
+// IPAddressTarget is a single IPv4 or IPv6 address.
+type IPAddressTargetApplyConfiguration struct {
+ // Address is a single IP address, e.g. "10.0.0.5" or "2001:db8::1".
+ // CIDR notation (e.g. "10.0.0.0/24") is rejected.
+ Address *string `json:"address,omitempty"`
+}
+
+// IPAddressTargetApplyConfiguration constructs a declarative configuration of the IPAddressTarget type for use with
+// apply.
+func IPAddressTarget() *IPAddressTargetApplyConfiguration {
+ return &IPAddressTargetApplyConfiguration{}
+}
+
+// WithAddress sets the Address field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Address field is set to the value of the last call.
+func (b *IPAddressTargetApplyConfiguration) WithAddress(value string) *IPAddressTargetApplyConfiguration {
+ b.Address = &value
+ return b
+}
diff --git a/pkg/applyconfigurations/api/v1alpha1/iptarget.go b/pkg/applyconfigurations/api/v1alpha1/iptarget.go
new file mode 100644
index 00000000..2e1132ff
--- /dev/null
+++ b/pkg/applyconfigurations/api/v1alpha1/iptarget.go
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// IPTargetApplyConfiguration represents a declarative configuration of the IPTarget type for use
+// with apply.
+//
+// IPTarget is a single IPv4 or IPv6 address.
+type IPTargetApplyConfiguration struct {
+ // Address is a single IP address.
+ Address *string `json:"address,omitempty"`
+}
+
+// IPTargetApplyConfiguration constructs a declarative configuration of the IPTarget type for use with
+// apply.
+func IPTarget() *IPTargetApplyConfiguration {
+ return &IPTargetApplyConfiguration{}
+}
+
+// WithAddress sets the Address field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Address field is set to the value of the last call.
+func (b *IPTargetApplyConfiguration) WithAddress(value string) *IPTargetApplyConfiguration {
+ b.Address = &value
+ return b
+}
diff --git a/pkg/applyconfigurations/api/v1alpha1/networkegress.go b/pkg/applyconfigurations/api/v1alpha1/networkegress.go
new file mode 100644
index 00000000..72b21a32
--- /dev/null
+++ b/pkg/applyconfigurations/api/v1alpha1/networkegress.go
@@ -0,0 +1,231 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ types "k8s.io/apimachinery/pkg/types"
+ v1 "k8s.io/client-go/applyconfigurations/meta/v1"
+)
+
+// NetworkEgressApplyConfiguration represents a declarative configuration of the NetworkEgress type for use
+// with apply.
+//
+// NetworkEgress is the Schema for the networkegresses API.
+type NetworkEgressApplyConfiguration struct {
+ v1.TypeMetaApplyConfiguration `json:",inline"`
+ *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"`
+ Spec *NetworkEgressSpecApplyConfiguration `json:"spec,omitempty"`
+ Status *NetworkEgressStatusApplyConfiguration `json:"status,omitempty"`
+}
+
+// NetworkEgress constructs a declarative configuration of the NetworkEgress type for use with
+// apply.
+func NetworkEgress(name, namespace string) *NetworkEgressApplyConfiguration {
+ b := &NetworkEgressApplyConfiguration{}
+ b.WithName(name)
+ b.WithNamespace(namespace)
+ b.WithKind("NetworkEgress")
+ b.WithAPIVersion("netbird.io/v1alpha1")
+ return b
+}
+
+func (b NetworkEgressApplyConfiguration) IsApplyConfiguration() {}
+
+// WithKind sets the Kind field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Kind field is set to the value of the last call.
+func (b *NetworkEgressApplyConfiguration) WithKind(value string) *NetworkEgressApplyConfiguration {
+ b.TypeMetaApplyConfiguration.Kind = &value
+ return b
+}
+
+// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the APIVersion field is set to the value of the last call.
+func (b *NetworkEgressApplyConfiguration) WithAPIVersion(value string) *NetworkEgressApplyConfiguration {
+ b.TypeMetaApplyConfiguration.APIVersion = &value
+ return b
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *NetworkEgressApplyConfiguration) WithName(value string) *NetworkEgressApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Name = &value
+ return b
+}
+
+// WithGenerateName sets the GenerateName field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the GenerateName field is set to the value of the last call.
+func (b *NetworkEgressApplyConfiguration) WithGenerateName(value string) *NetworkEgressApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.GenerateName = &value
+ return b
+}
+
+// WithNamespace sets the Namespace field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Namespace field is set to the value of the last call.
+func (b *NetworkEgressApplyConfiguration) WithNamespace(value string) *NetworkEgressApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Namespace = &value
+ return b
+}
+
+// WithUID sets the UID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the UID field is set to the value of the last call.
+func (b *NetworkEgressApplyConfiguration) WithUID(value types.UID) *NetworkEgressApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.UID = &value
+ return b
+}
+
+// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ResourceVersion field is set to the value of the last call.
+func (b *NetworkEgressApplyConfiguration) WithResourceVersion(value string) *NetworkEgressApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.ResourceVersion = &value
+ return b
+}
+
+// WithGeneration sets the Generation field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Generation field is set to the value of the last call.
+func (b *NetworkEgressApplyConfiguration) WithGeneration(value int64) *NetworkEgressApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Generation = &value
+ return b
+}
+
+// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the CreationTimestamp field is set to the value of the last call.
+func (b *NetworkEgressApplyConfiguration) WithCreationTimestamp(value metav1.Time) *NetworkEgressApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.CreationTimestamp = &value
+ return b
+}
+
+// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DeletionTimestamp field is set to the value of the last call.
+func (b *NetworkEgressApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *NetworkEgressApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value
+ return b
+}
+
+// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call.
+func (b *NetworkEgressApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *NetworkEgressApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value
+ return b
+}
+
+// WithLabels puts the entries into the Labels field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, the entries provided by each call will be put on the Labels field,
+// overwriting an existing map entries in Labels field with the same key.
+func (b *NetworkEgressApplyConfiguration) WithLabels(entries map[string]string) *NetworkEgressApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 {
+ b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries))
+ }
+ for k, v := range entries {
+ b.ObjectMetaApplyConfiguration.Labels[k] = v
+ }
+ return b
+}
+
+// WithAnnotations puts the entries into the Annotations field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, the entries provided by each call will be put on the Annotations field,
+// overwriting an existing map entries in Annotations field with the same key.
+func (b *NetworkEgressApplyConfiguration) WithAnnotations(entries map[string]string) *NetworkEgressApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 {
+ b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries))
+ }
+ for k, v := range entries {
+ b.ObjectMetaApplyConfiguration.Annotations[k] = v
+ }
+ return b
+}
+
+// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the OwnerReferences field.
+func (b *NetworkEgressApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *NetworkEgressApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ for i := range values {
+ if values[i] == nil {
+ panic("nil value passed to WithOwnerReferences")
+ }
+ b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i])
+ }
+ return b
+}
+
+// WithFinalizers adds the given value to the Finalizers field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Finalizers field.
+func (b *NetworkEgressApplyConfiguration) WithFinalizers(values ...string) *NetworkEgressApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ for i := range values {
+ b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i])
+ }
+ return b
+}
+
+func (b *NetworkEgressApplyConfiguration) ensureObjectMetaApplyConfigurationExists() {
+ if b.ObjectMetaApplyConfiguration == nil {
+ b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{}
+ }
+}
+
+// WithSpec sets the Spec field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Spec field is set to the value of the last call.
+func (b *NetworkEgressApplyConfiguration) WithSpec(value *NetworkEgressSpecApplyConfiguration) *NetworkEgressApplyConfiguration {
+ b.Spec = value
+ return b
+}
+
+// WithStatus sets the Status field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Status field is set to the value of the last call.
+func (b *NetworkEgressApplyConfiguration) WithStatus(value *NetworkEgressStatusApplyConfiguration) *NetworkEgressApplyConfiguration {
+ b.Status = value
+ return b
+}
+
+// GetKind retrieves the value of the Kind field in the declarative configuration.
+func (b *NetworkEgressApplyConfiguration) GetKind() *string {
+ return b.TypeMetaApplyConfiguration.Kind
+}
+
+// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration.
+func (b *NetworkEgressApplyConfiguration) GetAPIVersion() *string {
+ return b.TypeMetaApplyConfiguration.APIVersion
+}
+
+// GetName retrieves the value of the Name field in the declarative configuration.
+func (b *NetworkEgressApplyConfiguration) GetName() *string {
+ b.ensureObjectMetaApplyConfigurationExists()
+ return b.ObjectMetaApplyConfiguration.Name
+}
+
+// GetNamespace retrieves the value of the Namespace field in the declarative configuration.
+func (b *NetworkEgressApplyConfiguration) GetNamespace() *string {
+ b.ensureObjectMetaApplyConfigurationExists()
+ return b.ObjectMetaApplyConfiguration.Namespace
+}
diff --git a/pkg/applyconfigurations/api/v1alpha1/networkegressfqdntarget.go b/pkg/applyconfigurations/api/v1alpha1/networkegressfqdntarget.go
new file mode 100644
index 00000000..a60e9a8a
--- /dev/null
+++ b/pkg/applyconfigurations/api/v1alpha1/networkegressfqdntarget.go
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// NetworkEgressFQDNTargetApplyConfiguration represents a declarative configuration of the NetworkEgressFQDNTarget type for use
+// with apply.
+//
+// NetworkEgressFQDNTarget matches traffic by an exact domain name (no wildcards).
+type NetworkEgressFQDNTargetApplyConfiguration struct {
+ // Hostname is a fully qualified domain name to match exactly.
+ Hostname *string `json:"hostname,omitempty"`
+}
+
+// NetworkEgressFQDNTargetApplyConfiguration constructs a declarative configuration of the NetworkEgressFQDNTarget type for use with
+// apply.
+func NetworkEgressFQDNTarget() *NetworkEgressFQDNTargetApplyConfiguration {
+ return &NetworkEgressFQDNTargetApplyConfiguration{}
+}
+
+// WithHostname sets the Hostname field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Hostname field is set to the value of the last call.
+func (b *NetworkEgressFQDNTargetApplyConfiguration) WithHostname(value string) *NetworkEgressFQDNTargetApplyConfiguration {
+ b.Hostname = &value
+ return b
+}
diff --git a/pkg/applyconfigurations/api/v1alpha1/networkegressiptarget.go b/pkg/applyconfigurations/api/v1alpha1/networkegressiptarget.go
new file mode 100644
index 00000000..aa8c110e
--- /dev/null
+++ b/pkg/applyconfigurations/api/v1alpha1/networkegressiptarget.go
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// NetworkEgressIPTargetApplyConfiguration represents a declarative configuration of the NetworkEgressIPTarget type for use
+// with apply.
+//
+// NetworkEgressIPTarget is a single IPv4 or IPv6 address.
+type NetworkEgressIPTargetApplyConfiguration struct {
+ // Address is a single IP address.
+ Address *string `json:"address,omitempty"`
+}
+
+// NetworkEgressIPTargetApplyConfiguration constructs a declarative configuration of the NetworkEgressIPTarget type for use with
+// apply.
+func NetworkEgressIPTarget() *NetworkEgressIPTargetApplyConfiguration {
+ return &NetworkEgressIPTargetApplyConfiguration{}
+}
+
+// WithAddress sets the Address field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Address field is set to the value of the last call.
+func (b *NetworkEgressIPTargetApplyConfiguration) WithAddress(value string) *NetworkEgressIPTargetApplyConfiguration {
+ b.Address = &value
+ return b
+}
diff --git a/pkg/applyconfigurations/api/v1alpha1/networkegressport.go b/pkg/applyconfigurations/api/v1alpha1/networkegressport.go
new file mode 100644
index 00000000..fd0d4fc2
--- /dev/null
+++ b/pkg/applyconfigurations/api/v1alpha1/networkegressport.go
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// NetworkEgressPortApplyConfiguration represents a declarative configuration of the NetworkEgressPort type for use
+// with apply.
+type NetworkEgressPortApplyConfiguration struct {
+ // Name of the port.
+ Name *string `json:"name,omitempty"`
+ // The port that will be exposed by this service.
+ Port *int32 `json:"port,omitempty"`
+}
+
+// NetworkEgressPortApplyConfiguration constructs a declarative configuration of the NetworkEgressPort type for use with
+// apply.
+func NetworkEgressPort() *NetworkEgressPortApplyConfiguration {
+ return &NetworkEgressPortApplyConfiguration{}
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *NetworkEgressPortApplyConfiguration) WithName(value string) *NetworkEgressPortApplyConfiguration {
+ b.Name = &value
+ return b
+}
+
+// WithPort sets the Port field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Port field is set to the value of the last call.
+func (b *NetworkEgressPortApplyConfiguration) WithPort(value int32) *NetworkEgressPortApplyConfiguration {
+ b.Port = &value
+ return b
+}
diff --git a/pkg/applyconfigurations/api/v1alpha1/networkegressspec.go b/pkg/applyconfigurations/api/v1alpha1/networkegressspec.go
new file mode 100644
index 00000000..7b51b873
--- /dev/null
+++ b/pkg/applyconfigurations/api/v1alpha1/networkegressspec.go
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// NetworkEgressSpecApplyConfiguration represents a declarative configuration of the NetworkEgressSpec type for use
+// with apply.
+//
+// NetworkEgressSpec defines the desired state of NetworkEgress.
+type NetworkEgressSpecApplyConfiguration struct {
+ // NetworkRouterRef is a reference to the network and router where the resource will be created.
+ NetworkRouterRef *CrossNamespaceReferenceApplyConfiguration `json:"networkRouterRef,omitempty"`
+ // Target for egress traffic.
+ Target *NetworkEgressTargetApplyConfiguration `json:"target,omitempty"`
+ // Ports to the resource to route.
+ Ports []NetworkEgressPortApplyConfiguration `json:"ports,omitempty"`
+}
+
+// NetworkEgressSpecApplyConfiguration constructs a declarative configuration of the NetworkEgressSpec type for use with
+// apply.
+func NetworkEgressSpec() *NetworkEgressSpecApplyConfiguration {
+ return &NetworkEgressSpecApplyConfiguration{}
+}
+
+// WithNetworkRouterRef sets the NetworkRouterRef field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the NetworkRouterRef field is set to the value of the last call.
+func (b *NetworkEgressSpecApplyConfiguration) WithNetworkRouterRef(value *CrossNamespaceReferenceApplyConfiguration) *NetworkEgressSpecApplyConfiguration {
+ b.NetworkRouterRef = value
+ return b
+}
+
+// WithTarget sets the Target field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Target field is set to the value of the last call.
+func (b *NetworkEgressSpecApplyConfiguration) WithTarget(value *NetworkEgressTargetApplyConfiguration) *NetworkEgressSpecApplyConfiguration {
+ b.Target = value
+ return b
+}
+
+// WithPorts adds the given value to the Ports field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Ports field.
+func (b *NetworkEgressSpecApplyConfiguration) WithPorts(values ...*NetworkEgressPortApplyConfiguration) *NetworkEgressSpecApplyConfiguration {
+ for i := range values {
+ if values[i] == nil {
+ panic("nil value passed to WithPorts")
+ }
+ b.Ports = append(b.Ports, *values[i])
+ }
+ return b
+}
diff --git a/pkg/applyconfigurations/api/v1alpha1/networkegressstatus.go b/pkg/applyconfigurations/api/v1alpha1/networkegressstatus.go
new file mode 100644
index 00000000..25e65437
--- /dev/null
+++ b/pkg/applyconfigurations/api/v1alpha1/networkegressstatus.go
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ v1 "k8s.io/client-go/applyconfigurations/meta/v1"
+)
+
+// NetworkEgressStatusApplyConfiguration represents a declarative configuration of the NetworkEgressStatus type for use
+// with apply.
+//
+// NetworkEgressStatus defines the observed state of NetworkEgress.
+type NetworkEgressStatusApplyConfiguration struct {
+ // ObservedGeneration is the last reconciled generation.
+ ObservedGeneration *int64 `json:"observedGeneration,omitempty"`
+ // Conditions holds the conditions for the NetworkEgress.
+ Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"`
+}
+
+// NetworkEgressStatusApplyConfiguration constructs a declarative configuration of the NetworkEgressStatus type for use with
+// apply.
+func NetworkEgressStatus() *NetworkEgressStatusApplyConfiguration {
+ return &NetworkEgressStatusApplyConfiguration{}
+}
+
+// WithObservedGeneration sets the ObservedGeneration field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ObservedGeneration field is set to the value of the last call.
+func (b *NetworkEgressStatusApplyConfiguration) WithObservedGeneration(value int64) *NetworkEgressStatusApplyConfiguration {
+ b.ObservedGeneration = &value
+ return b
+}
+
+// WithConditions adds the given value to the Conditions field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Conditions field.
+func (b *NetworkEgressStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *NetworkEgressStatusApplyConfiguration {
+ for i := range values {
+ if values[i] == nil {
+ panic("nil value passed to WithConditions")
+ }
+ b.Conditions = append(b.Conditions, *values[i])
+ }
+ return b
+}
diff --git a/pkg/applyconfigurations/api/v1alpha1/networkegresstarget.go b/pkg/applyconfigurations/api/v1alpha1/networkegresstarget.go
new file mode 100644
index 00000000..364e3faf
--- /dev/null
+++ b/pkg/applyconfigurations/api/v1alpha1/networkegresstarget.go
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// NetworkEgressTargetApplyConfiguration represents a declarative configuration of the NetworkEgressTarget type for use
+// with apply.
+//
+// NetworkEgressTarget describes a single allowed egress destination.
+// Exactly one of IP or FQDN must be set.
+type NetworkEgressTargetApplyConfiguration struct {
+ // IP targets a single specific IP address (not a CIDR range).
+ IP *NetworkEgressIPTargetApplyConfiguration `json:"ip,omitempty"`
+ // FQDN targets an exact domain name (no wildcards).
+ FQDN *NetworkEgressFQDNTargetApplyConfiguration `json:"fqdn,omitempty"`
+}
+
+// NetworkEgressTargetApplyConfiguration constructs a declarative configuration of the NetworkEgressTarget type for use with
+// apply.
+func NetworkEgressTarget() *NetworkEgressTargetApplyConfiguration {
+ return &NetworkEgressTargetApplyConfiguration{}
+}
+
+// WithIP sets the IP field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the IP field is set to the value of the last call.
+func (b *NetworkEgressTargetApplyConfiguration) WithIP(value *NetworkEgressIPTargetApplyConfiguration) *NetworkEgressTargetApplyConfiguration {
+ b.IP = value
+ return b
+}
+
+// WithFQDN sets the FQDN field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the FQDN field is set to the value of the last call.
+func (b *NetworkEgressTargetApplyConfiguration) WithFQDN(value *NetworkEgressFQDNTargetApplyConfiguration) *NetworkEgressTargetApplyConfiguration {
+ b.FQDN = value
+ return b
+}
diff --git a/pkg/applyconfigurations/utils.go b/pkg/applyconfigurations/utils.go
index 7a18c179..487364b2 100644
--- a/pkg/applyconfigurations/utils.go
+++ b/pkg/applyconfigurations/utils.go
@@ -38,6 +38,20 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &apiv1alpha1.GroupSpecApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("GroupStatus"):
return &apiv1alpha1.GroupStatusApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("NetworkEgress"):
+ return &apiv1alpha1.NetworkEgressApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("NetworkEgressFQDNTarget"):
+ return &apiv1alpha1.NetworkEgressFQDNTargetApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("NetworkEgressIPTarget"):
+ return &apiv1alpha1.NetworkEgressIPTargetApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("NetworkEgressPort"):
+ return &apiv1alpha1.NetworkEgressPortApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("NetworkEgressSpec"):
+ return &apiv1alpha1.NetworkEgressSpecApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("NetworkEgressStatus"):
+ return &apiv1alpha1.NetworkEgressStatusApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("NetworkEgressTarget"):
+ return &apiv1alpha1.NetworkEgressTargetApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("NetworkResource"):
return &apiv1alpha1.NetworkResourceApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("NetworkResourceSpec"):
diff --git a/pkg/version/version.go b/pkg/version/version.go
index b46a167b..fe84930c 100644
--- a/pkg/version/version.go
+++ b/pkg/version/version.go
@@ -7,8 +7,9 @@ import (
)
const (
- NetbirdClientImage = "ghcr.io/netbirdio/netbird:0.72.4@sha256:6c6c20baffae4a3ec50f29ec9361608a420625185505e8cd6f0c44d71c5d4798"
- KubeApiProxyImage = "ghcr.io/netbirdio/netbird-kubeapi-proxy:v0.0.4@sha256:bffa4f093abc19b4934ae37657bac76fa3b390cbd39aadac987634215eb750f5"
+ NetbirdClientImage = "ghcr.io/netbirdio/netbird:0.72.4@sha256:6c6c20baffae4a3ec50f29ec9361608a420625185505e8cd6f0c44d71c5d4798"
+ KubeApiProxyImage = "ghcr.io/netbirdio/netbird-kubeapi-proxy:v0.0.4@sha256:bffa4f093abc19b4934ae37657bac76fa3b390cbd39aadac987634215eb750f5"
+ KubeEgressForwarderImage = "ghcr.io/netbirdio/kube-egress-forwarder:v0.0.2@sha256:f3b4637122cbda3c1915d49e6f96edff7e3a3accfceb87b821c829056abe8a6f"
)
func BuildVersion() string {
diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go
index 666c9bb3..a3a9c23b 100644
--- a/test/e2e/e2e_test.go
+++ b/test/e2e/e2e_test.go
@@ -25,7 +25,7 @@ import (
netbird "github.com/netbirdio/netbird/shared/management/client/rest"
"github.com/netbirdio/netbird/shared/management/http/api"
"helm.sh/helm/v4/pkg/action"
- "helm.sh/helm/v4/pkg/chart/loader"
+ "helm.sh/helm/v4/pkg/chart/v2/loader"
"helm.sh/helm/v4/pkg/downloader"
"helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v4/pkg/kube"
@@ -38,6 +38,7 @@ import (
kruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
kruntimeutil "k8s.io/apimachinery/pkg/util/runtime"
+ "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -290,8 +291,8 @@ func TestE2E(t *testing.T) {
}
err = k8sClient.Create(t.Context(), secret)
require.NoError(t, err)
- installOperator(t, kcPath, false, managementURL)
- installOperator(t, kcPath, true, managementURL)
+ installOperator(t, k8sClient, kcPath, false, managementURL)
+ installOperator(t, k8sClient, kcPath, true, managementURL)
t.Run("cluster proxy", testClusterProxy(k8sClient, nbClient))
})
@@ -393,7 +394,7 @@ func testClusterProxy(k8sClient client.Client, nbClient *netbird.Client) func(*t
}
}
-func installOperator(t *testing.T, kcPath string, dev bool, managementURL string) {
+func installOperator(t *testing.T, k8sClient client.Client, kcPath string, dev bool, managementURL string) {
t.Helper()
regClient, err := registry.NewClient()
@@ -455,6 +456,15 @@ func installOperator(t *testing.T, kcPath string, dev bool, managementURL string
_, err = install.RunWithContext(t.Context(), charter, vals)
require.NoError(t, err)
} else {
+ for _, crd := range charter.CRDObjects() {
+ docs := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(crd.File.Data), 4096)
+ var obj unstructured.Unstructured
+ err := docs.Decode(&obj)
+ require.NoError(t, err)
+ err = k8sClient.Patch(t.Context(), &obj, client.Apply, client.ForceOwnership, client.FieldOwner("helm"))
+ require.NoError(t, err)
+ }
+
upgrade := action.NewUpgrade(actionCfg)
upgrade.Namespace = netbirdNamespace
upgrade.WaitStrategy = kube.StatusWatcherStrategy