Add network egress resource (#357)

This change adds a new import resource which enables exposing Netbird
resources as Kubernetes services. This remove the need to add sidecars
to every pod.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a new `NetworkEgress` custom resource (`netbird.io/v1alpha1`)
with CRD, schema validation, and status/conditions.
* Extended controller functionality to create egress services and
translate egress rules into import `EndpointSlice` resources; egress
pods now include a kube-egress-forwarder sidecar.
* **Bug Fixes**
* Added missing deep-copy and declarative apply support for the new
`NetworkEgress` API types.
* **Documentation**
* Updated README/API reference and added example manifests for
`NetworkEgress` (including IP/FQDN target usage).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Philip Laine <philip.laine@gmail.com>
This commit is contained in:
Philip Laine
2026-07-17 08:06:53 +02:00
committed by GitHub
parent 3c1c6675d9
commit ea2dafd5a3
31 changed files with 2220 additions and 83 deletions
+120
View File
@@ -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{})
}
+173
View File
@@ -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