From 3d56f6ecfaaf23f82aeede5eaf4aa2d2905b666f Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Fri, 23 Oct 2026 15:59:24 +0000 Subject: [PATCH] feat: Add the network load balancer API Add the resource customers use to create a network load balancer: TCP and UDP listeners, the backend services that receive traffic, health check settings, and per-location address and hostname status. Key changes: - Add NetworkLoadBalancer and NetworkLoadBalancerRoute types and CRDs - Refuse options the platform cannot serve yet (IPv4, TLS, multiple backends, cross-namespace references) with a clear admission message - Regenerate CRDs and API reference docs --- api/v1alpha/groupversion_info.go | 4 + .../networkloadbalancer_schema_test.go | 148 ++++ api/v1alpha/networkloadbalancer_types.go | 240 ++++++ api/v1alpha/networkloadbalancerroute_types.go | 153 ++++ api/v1alpha/zz_generated.deepcopy.go | 583 +++++++++++++ ...tumapis.com_networkloadbalancerroutes.yaml | 279 ++++++ ...ng.datumapis.com_networkloadbalancers.yaml | 454 ++++++++++ config/crd/kustomization.yaml | 2 + .../quota/claim-policies/kustomization.yaml | 2 + .../networkloadbalancer-claim-policy.yaml | 26 + ...networkloadbalancerroute-claim-policy.yaml | 26 + config/quota/registrations/kustomization.yaml | 2 + .../networkloadbalancer-registration.yaml | 24 + ...networkloadbalancerroute-registration.yaml | 24 + config/webhook/manifests.yaml | 40 + docs/api/networkloadbalancerroutes.md | 565 ++++++++++++ docs/api/networkloadbalancers.md | 809 ++++++++++++++++++ internal/agent/catalog_test.go | 30 +- internal/cmd/manager/manager.go | 6 + internal/cmd/manager/manager_test.go | 2 + internal/networkagent/catalog_test.go | 20 +- .../v1alpha/networkloadbalancer_webhook.go | 72 ++ .../networkloadbalancer_webhook_test.go | 60 ++ .../networkloadbalancerroute_webhook.go | 87 ++ 24 files changed, 3635 insertions(+), 23 deletions(-) create mode 100644 api/v1alpha/networkloadbalancer_schema_test.go create mode 100644 api/v1alpha/networkloadbalancer_types.go create mode 100644 api/v1alpha/networkloadbalancerroute_types.go create mode 100644 config/crd/bases/networking.datumapis.com_networkloadbalancerroutes.yaml create mode 100644 config/crd/bases/networking.datumapis.com_networkloadbalancers.yaml create mode 100644 config/quota/claim-policies/networkloadbalancer-claim-policy.yaml create mode 100644 config/quota/claim-policies/networkloadbalancerroute-claim-policy.yaml create mode 100644 config/quota/registrations/networkloadbalancer-registration.yaml create mode 100644 config/quota/registrations/networkloadbalancerroute-registration.yaml create mode 100644 docs/api/networkloadbalancerroutes.md create mode 100644 docs/api/networkloadbalancers.md create mode 100644 internal/webhook/v1alpha/networkloadbalancer_webhook.go create mode 100644 internal/webhook/v1alpha/networkloadbalancer_webhook_test.go create mode 100644 internal/webhook/v1alpha/networkloadbalancerroute_webhook.go diff --git a/api/v1alpha/groupversion_info.go b/api/v1alpha/groupversion_info.go index c63cb446..4f890ace 100644 --- a/api/v1alpha/groupversion_info.go +++ b/api/v1alpha/groupversion_info.go @@ -42,6 +42,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { &NetworkInterfaceList{}, &NetworkInterfaceClaim{}, &NetworkInterfaceClaimList{}, + &NetworkLoadBalancer{}, + &NetworkLoadBalancerList{}, + &NetworkLoadBalancerRoute{}, + &NetworkLoadBalancerRouteList{}, &NetworkPolicy{}, &NetworkPolicyList{}, &NetworkService{}, diff --git a/api/v1alpha/networkloadbalancer_schema_test.go b/api/v1alpha/networkloadbalancer_schema_test.go new file mode 100644 index 00000000..125d7f01 --- /dev/null +++ b/api/v1alpha/networkloadbalancer_schema_test.go @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha_test + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/yaml" +) + +func TestNetworkLoadBalancerAdmission(t *testing.T) { + if os.Getenv("KUBEBUILDER_ASSETS") == "" { + t.Skip("envtest assets are required") + } + + crds := make([]*apiextensionsv1.CustomResourceDefinition, 0, 2) + for _, name := range []string{"networkloadbalancers", "networkloadbalancerroutes"} { + data, err := os.ReadFile(filepath.Join("..", "..", "config", "crd", "bases", "networking.datumapis.com_"+name+".yaml")) + require.NoError(t, err) + var crd apiextensionsv1.CustomResourceDefinition + require.NoError(t, yaml.Unmarshal(data, &crd)) + crds = append(crds, &crd) + } + + env := &envtest.Environment{CRDs: crds} + cfg, err := env.Start() + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, env.Stop()) }) + + scheme := runtime.NewScheme() + require.NoError(t, networkingv1alpha.AddToScheme(scheme)) + k8sClient, err := client.New(cfg, client.Options{Scheme: scheme}) + require.NoError(t, err) + + base := &networkingv1alpha.NetworkLoadBalancer{ + ObjectMeta: metav1.ObjectMeta{Name: "valid", Namespace: "default"}, + Spec: networkingv1alpha.NetworkLoadBalancerSpec{ + NetworkRef: networkingv1alpha.LocalNetworkRef{Name: "default"}, + Exposure: networkingv1alpha.NetworkLoadBalancerExposure{Addresses: []networkingv1alpha.NetworkLoadBalancerAddressRequest{{ + Name: "regional", + Families: []networkingv1alpha.IPFamily{networkingv1alpha.IPv6Protocol}, + }}}, + Listeners: []networkingv1alpha.NetworkLoadBalancerListener{ + {Name: "tcp", Protocol: networkingv1alpha.NetworkLoadBalancerProtocolTCP, Port: 443, AddressRefs: []string{"regional"}}, + {Name: "udp", Protocol: networkingv1alpha.NetworkLoadBalancerProtocolUDP, Port: 443, AddressRefs: []string{"regional"}}, + }, + }, + } + + tests := []struct { + name string + mutate func(*networkingv1alpha.NetworkLoadBalancer) + valid bool + }{ + {name: "TCP and UDP share a port", valid: true}, + {name: "IPv4 retained in API for capability gating", valid: true, mutate: func(lb *networkingv1alpha.NetworkLoadBalancer) { + lb.Spec.Exposure.Addresses[0].Families = []networkingv1alpha.IPFamily{networkingv1alpha.IPv4Protocol, networkingv1alpha.IPv6Protocol} + }}, + {name: "duplicate family rejected", mutate: func(lb *networkingv1alpha.NetworkLoadBalancer) { + lb.Spec.Exposure.Addresses[0].Families = []networkingv1alpha.IPFamily{networkingv1alpha.IPv6Protocol, networkingv1alpha.IPv6Protocol} + }}, + {name: "missing address reference", mutate: func(lb *networkingv1alpha.NetworkLoadBalancer) { + lb.Spec.Listeners[0].AddressRefs = []string{"missing"} + }}, + {name: "same transport and port conflict", mutate: func(lb *networkingv1alpha.NetworkLoadBalancer) { + lb.Spec.Listeners = append(lb.Spec.Listeners, networkingv1alpha.NetworkLoadBalancerListener{Name: "second-tcp", Protocol: networkingv1alpha.NetworkLoadBalancerProtocolTCP, Port: 443, AddressRefs: []string{"regional"}}) + }}, + {name: "Names requires location names", mutate: func(lb *networkingv1alpha.NetworkLoadBalancer) { + lb.Spec.Exposure.Addresses[0].Locations = &networkingv1alpha.NetworkLoadBalancerLocationSelection{From: networkingv1alpha.NetworkLoadBalancerLocationSourceNames} + }}, + {name: "All excludes location names", mutate: func(lb *networkingv1alpha.NetworkLoadBalancer) { + lb.Spec.Exposure.Addresses[0].Locations = &networkingv1alpha.NetworkLoadBalancerLocationSelection{From: networkingv1alpha.NetworkLoadBalancerLocationSourceAll, Names: []string{"us-east-1"}} + }}, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + lb := base.DeepCopy() + lb.Name = fmt.Sprintf("case-%d", i) + if tt.mutate != nil { + tt.mutate(lb) + } + err := k8sClient.Create(context.Background(), lb) + if tt.valid { + require.NoError(t, err) + lb.Status.Endpoints = []networkingv1alpha.NetworkLoadBalancerEndpointStatus{ + {RequestName: "regional", Location: "us-east-1", Addresses: []networkingv1alpha.NetworkLoadBalancerAddressStatus{{Family: networkingv1alpha.IPv6Protocol, Value: "2001:db8::10"}}}, + {RequestName: "regional", Location: "us-west-1", Addresses: []networkingv1alpha.NetworkLoadBalancerAddressStatus{{Family: networkingv1alpha.IPv6Protocol, Value: "2001:db8::20"}}}, + } + require.NoError(t, k8sClient.Status().Update(context.Background(), lb)) + } else { + require.Error(t, err) + } + }) + } + + route := &networkingv1alpha.NetworkLoadBalancerRoute{ + ObjectMeta: metav1.ObjectMeta{Namespace: "default"}, + Spec: networkingv1alpha.NetworkLoadBalancerRouteSpec{ + ParentRefs: []networkingv1alpha.NetworkLoadBalancerRouteParentRef{{Name: "valid", Listener: "tcp"}}, + Backends: []networkingv1alpha.NetworkLoadBalancerRouteBackend{{ + NetworkService: networkingv1alpha.NetworkLoadBalancerNetworkServiceRef{Name: "app", Port: "main"}, + }}, + }, + } + + routeTests := []struct { + name string + mutate func(*networkingv1alpha.NetworkLoadBalancerRoute) + valid bool + }{ + {name: "valid route", valid: true}, + {name: "multiple backends retained in API", valid: true, mutate: func(r *networkingv1alpha.NetworkLoadBalancerRoute) { + r.Spec.Backends = append(r.Spec.Backends, networkingv1alpha.NetworkLoadBalancerRouteBackend{NetworkService: networkingv1alpha.NetworkLoadBalancerNetworkServiceRef{Name: "other", Port: "main"}}) + }}, + {name: "TCP path rejected", mutate: func(r *networkingv1alpha.NetworkLoadBalancerRoute) { + r.Spec.Backends[0].HealthCheck = &networkingv1alpha.NetworkLoadBalancerHealthCheck{Protocol: "TCP", Path: "/health"} + }}, + {name: "HTTP probe requires port", mutate: func(r *networkingv1alpha.NetworkLoadBalancerRoute) { + r.Spec.Backends[0].HealthCheck = &networkingv1alpha.NetworkLoadBalancerHealthCheck{Protocol: "HTTP", Path: "/health"} + }}, + } + for i, tt := range routeTests { + t.Run(tt.name, func(t *testing.T) { + r := route.DeepCopy() + r.Name = fmt.Sprintf("route-%d", i) + if tt.mutate != nil { + tt.mutate(r) + } + err := k8sClient.Create(context.Background(), r) + if tt.valid { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} diff --git a/api/v1alpha/networkloadbalancer_types.go b/api/v1alpha/networkloadbalancer_types.go new file mode 100644 index 00000000..12840fb0 --- /dev/null +++ b/api/v1alpha/networkloadbalancer_types.go @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +const ( + NetworkLoadBalancerAllocated = "Allocated" + NetworkLoadBalancerProgrammed = "Programmed" + NetworkLoadBalancerReady = "Ready" + NetworkLoadBalancerAnnounced = "Announced" + NetworkLoadBalancerDNSPublished = "DNSPublished" +) + +// NetworkLoadBalancerLocationSource selects locations by name or follows all +// eligible locations as they become available. +// +kubebuilder:validation:Enum=Backends;All;Names +type NetworkLoadBalancerLocationSource string + +const ( + NetworkLoadBalancerLocationSourceBackends NetworkLoadBalancerLocationSource = "Backends" + NetworkLoadBalancerLocationSourceAll NetworkLoadBalancerLocationSource = "All" + NetworkLoadBalancerLocationSourceNames NetworkLoadBalancerLocationSource = "Names" +) + +// NetworkLoadBalancerProtocol is the transport accepted by a listener. +// +kubebuilder:validation:Enum=TCP;UDP;TLS +type NetworkLoadBalancerProtocol string + +const ( + NetworkLoadBalancerProtocolTCP NetworkLoadBalancerProtocol = "TCP" + NetworkLoadBalancerProtocolUDP NetworkLoadBalancerProtocol = "UDP" + NetworkLoadBalancerProtocolTLS NetworkLoadBalancerProtocol = "TLS" +) + +// NetworkLoadBalancerLocationSelection chooses where an address may be served. +// +kubebuilder:validation:XValidation:message="names must be set only when from is Names",rule="self.from == 'Names' ? has(self.names) && size(self.names) > 0 : !has(self.names)" +type NetworkLoadBalancerLocationSelection struct { + // +kubebuilder:validation:Required + From NetworkLoadBalancerLocationSource `json:"from"` + + // +kubebuilder:validation:Optional + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=64 + // +listType=set + Names []string `json:"names,omitempty"` +} + +// NetworkLoadBalancerAddressRequest reserves one public address per selected +// location, independently of whether a route has healthy backends. +type NetworkLoadBalancerAddressRequest struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` + Name string `json:"name"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=2 + // +listType=set + Families []IPFamily `json:"families"` + + // Omission follows the effective backends' network interfaces. + // +kubebuilder:validation:Optional + Locations *NetworkLoadBalancerLocationSelection `json:"locations,omitempty"` +} + +type NetworkLoadBalancerTLSMode string + +const ( + NetworkLoadBalancerTLSModePassthrough NetworkLoadBalancerTLSMode = "Passthrough" + NetworkLoadBalancerTLSModeTerminate NetworkLoadBalancerTLSMode = "Terminate" +) + +// +kubebuilder:validation:XValidation:message="Terminate requires a certificate; Passthrough omits it",rule="self.mode == 'Terminate' ? has(self.certificateRef) : !has(self.certificateRef)" +type NetworkLoadBalancerTLS struct { + // +kubebuilder:validation:Enum=Passthrough;Terminate + Mode NetworkLoadBalancerTLSMode `json:"mode"` + // Secret references are admitted only after TLS termination is supported. + CertificateRef *LocalSecretRef `json:"certificateRef,omitempty"` +} + +type LocalSecretRef struct { + // Name identifies a Secret in the listener's namespace. + Name string `json:"name"` + // Cross-namespace access is capability-gated until grants are supported. + Namespace string `json:"namespace,omitempty"` +} + +// NetworkLoadBalancerExposure contains the load balancer's address requests. +type NetworkLoadBalancerExposure struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + // +listType=map + // +listMapKey=name + Addresses []NetworkLoadBalancerAddressRequest `json:"addresses"` +} + +// NetworkLoadBalancerListener accepts one transport and port on its named +// address requests. TCP and UDP may use the same port. +type NetworkLoadBalancerListener struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` + Name string `json:"name"` + + // +kubebuilder:validation:Required + Protocol NetworkLoadBalancerProtocol `json:"protocol"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + Port int32 `json:"port"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + // +listType=set + AddressRefs []string `json:"addressRefs"` + + // +kubebuilder:validation:Optional + Hostname string `json:"hostname,omitempty"` + + // +kubebuilder:validation:Optional + TLS *NetworkLoadBalancerTLS `json:"tls,omitempty"` +} + +// NetworkLoadBalancerSpec defines the network, public addresses, and listeners. +// +kubebuilder:validation:XValidation:message="each listener address reference must name an address request",rule="self.listeners.all(l, l.addressRefs.all(r, r in self.exposure.addresses.map(a, a.name)))" +// +kubebuilder:validation:XValidation:message="listeners sharing an address cannot use the same protocol and port",rule="self.listeners.all(l1, self.listeners.all(l2, l1.name == l2.name || l1.protocol != l2.protocol || l1.port != l2.port || !l1.addressRefs.exists(r, r in l2.addressRefs)))" +// +kubebuilder:validation:XValidation:message="TLS configuration is required only for TLS listeners",rule="self.listeners.all(l, l.protocol == 'TLS' ? has(l.tls) : !has(l.tls))" +type NetworkLoadBalancerSpec struct { + // The network must exist in the load balancer's namespace. + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:message="networkRef is immutable",rule="self == oldSelf" + NetworkRef LocalNetworkRef `json:"networkRef"` + + // +kubebuilder:validation:Required + Exposure NetworkLoadBalancerExposure `json:"exposure"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + // +listType=map + // +listMapKey=name + Listeners []NetworkLoadBalancerListener `json:"listeners"` +} + +// NetworkLoadBalancerIPClaimRef identifies the claim holding an allocated IP. +type NetworkLoadBalancerIPClaimRef struct { + Project string `json:"project"` + Namespace string `json:"namespace"` + Name string `json:"name"` + // UID distinguishes a recreated claim with the same name from the old allocation. + UID string `json:"uid,omitempty"` +} + +// NetworkLoadBalancerAddressStatus describes one family at a location. +type NetworkLoadBalancerAddressStatus struct { + Family IPFamily `json:"family"` + Value string `json:"value,omitempty"` + ClaimRef *NetworkLoadBalancerIPClaimRef `json:"claimRef,omitempty"` + Announced bool `json:"announced,omitempty"` + DNSPublished bool `json:"dnsPublished,omitempty"` + Reason string `json:"reason,omitempty"` + Message string `json:"message,omitempty"` + + // Conditions include Allocated, Programmed, and Announced with + // observedGeneration. An entry with no value reports a pending or failed + // allocation for this request/location. + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// NetworkLoadBalancerEndpointStatus is stable for one address request and +// location, even while individual address families are unavailable. +type NetworkLoadBalancerEndpointStatus struct { + RequestName string `json:"requestName"` + Location string `json:"location"` + CanonicalHostname string `json:"canonicalHostname,omitempty"` + // +listType=map + // +listMapKey=family + Addresses []NetworkLoadBalancerAddressStatus `json:"addresses,omitempty"` +} + +type NetworkLoadBalancerAddressRequestStatus struct { + Name string `json:"name"` + LocationSource NetworkLoadBalancerLocationSource `json:"locationSource"` + // +listType=set + ResolvedLocations []string `json:"resolvedLocations,omitempty"` +} + +// NetworkLoadBalancerStatus distinguishes address allocation from programming +// and announcement. Allocated IPs remain here during backend failures. +type NetworkLoadBalancerStatus struct { + // Conditions include Allocated, Programmed, and Ready with observedGeneration. + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // +kubebuilder:validation:MaxItems=16 + // +listType=map + // +listMapKey=name + AddressRequests []NetworkLoadBalancerAddressRequestStatus `json:"addressRequests,omitempty"` + + // +kubebuilder:validation:MaxItems=1024 + // +listType=map + // +listMapKey=requestName + // +listMapKey=location + Endpoints []NetworkLoadBalancerEndpointStatus `json:"endpoints,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" + +// NetworkLoadBalancer reserves public IPs and accepts TCP or UDP traffic on +// named listeners. Route objects attach backends to those listeners. +type NetworkLoadBalancer struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // +kubebuilder:validation:Required + Spec NetworkLoadBalancerSpec `json:"spec"` + Status NetworkLoadBalancerStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// NetworkLoadBalancerList contains a list of NetworkLoadBalancer objects. +type NetworkLoadBalancerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []NetworkLoadBalancer `json:"items"` +} diff --git a/api/v1alpha/networkloadbalancerroute_types.go b/api/v1alpha/networkloadbalancerroute_types.go new file mode 100644 index 00000000..fd24b0d9 --- /dev/null +++ b/api/v1alpha/networkloadbalancerroute_types.go @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +const ( + NetworkLoadBalancerRouteAccepted = "Accepted" + NetworkLoadBalancerRouteResolvedRefs = "ResolvedRefs" + NetworkLoadBalancerRouteProgrammed = "Programmed" +) + +// NetworkLoadBalancerRouteParentRef attaches a route to a listener. Cross- +// namespace references are capability-gated initially. Either listener name or +// port selects a listener; status always reports its resolved name. +// +kubebuilder:validation:XValidation:message="exactly one of listener or port is required",rule="has(self.listener) != has(self.port)" +type NetworkLoadBalancerRouteParentRef struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Name string `json:"name"` + + // One of listener or port selects the parent listener. + // +kubebuilder:validation:Optional + Listener string `json:"listener,omitempty"` + // +kubebuilder:validation:Optional + Port *int32 `json:"port,omitempty"` + // +kubebuilder:validation:Optional + Namespace string `json:"namespace,omitempty"` +} + +// NetworkLoadBalancerNetworkServiceRef names a backend service and one of its +// named ports in the route's namespace. +type NetworkLoadBalancerNetworkServiceRef struct { + // +kubebuilder:validation:Required + Name string `json:"name"` + + // +kubebuilder:validation:Required + Port string `json:"port"` + // +kubebuilder:validation:Optional + Namespace string `json:"namespace,omitempty"` +} + +// NetworkLoadBalancerHealthCheck checks an individual backend endpoint. UDP +// backends require a separate TCP, HTTP, or HTTPS health endpoint. +// +kubebuilder:validation:XValidation:message="path is only valid for HTTP and HTTPS probes",rule="has(self.path) ? self.protocol in ['HTTP', 'HTTPS'] : true" +// +kubebuilder:validation:XValidation:message="HTTP and HTTPS probes require a port",rule="self.protocol == 'TCP' || has(self.port)" +type NetworkLoadBalancerHealthCheck struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=TCP;HTTP;HTTPS + Protocol string `json:"protocol"` + + // Name of the probe port on the referenced NetworkService. When omitted, + // TCP probes use the backend's serving port. + // +kubebuilder:validation:Optional + Port string `json:"port,omitempty"` + + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Pattern=`^/.*$` + Path string `json:"path,omitempty"` + // +kubebuilder:validation:Minimum=1 + IntervalSeconds *int32 `json:"intervalSeconds,omitempty"` + // +kubebuilder:validation:Minimum=1 + TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` + // +kubebuilder:validation:Minimum=1 + HealthyThreshold *int32 `json:"healthyThreshold,omitempty"` + // +kubebuilder:validation:Minimum=1 + UnhealthyThreshold *int32 `json:"unhealthyThreshold,omitempty"` +} + +// NetworkLoadBalancerRouteBackend forwards new connections or flows to a +// named NetworkService port. +type NetworkLoadBalancerRouteBackend struct { + // +kubebuilder:validation:Required + NetworkService NetworkLoadBalancerNetworkServiceRef `json:"networkService"` + + // +kubebuilder:validation:Optional + HealthCheck *NetworkLoadBalancerHealthCheck `json:"healthCheck,omitempty"` + // Backend TLS requires a proxy-capable serving path. + // +kubebuilder:validation:Optional + Transport *NetworkLoadBalancerBackendTransport `json:"transport,omitempty"` +} + +type NetworkLoadBalancerBackendTransport struct { + // +kubebuilder:validation:Enum=TCP;TLS + Protocol string `json:"protocol"` + ServerName string `json:"serverName,omitempty"` + CABundleRef *LocalSecretRef `json:"caBundleRef,omitempty"` +} + +// NetworkLoadBalancerRouteSpec attaches backends to named listeners. +type NetworkLoadBalancerRouteSpec struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + // +listType=atomic + ParentRefs []NetworkLoadBalancerRouteParentRef `json:"parentRefs"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + Backends []NetworkLoadBalancerRouteBackend `json:"backends"` + + // SNI routing is capability-gated until TLS listeners are served. + // +kubebuilder:validation:Optional + SNIHostnames []string `json:"sniHostnames,omitempty"` +} + +// NetworkLoadBalancerRouteParentStatus reports attachment and effectiveness +// for one parent listener. The oldest accepted catch-all route is effective. +type NetworkLoadBalancerRouteParentStatus struct { + Name string `json:"name"` + Listener string `json:"listener"` + Effective bool `json:"effective"` + + // Conditions include Accepted, ResolvedRefs, and Programmed with + // observedGeneration. + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +type NetworkLoadBalancerRouteStatus struct { + // +kubebuilder:validation:MaxItems=16 + // +listType=map + // +listMapKey=name + // +listMapKey=listener + Parents []NetworkLoadBalancerRouteParentStatus `json:"parents,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" + +// NetworkLoadBalancerRoute connects load balancer listeners to NetworkService +// backends. References are restricted to the route's namespace. +type NetworkLoadBalancerRoute struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // +kubebuilder:validation:Required + Spec NetworkLoadBalancerRouteSpec `json:"spec"` + Status NetworkLoadBalancerRouteStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// NetworkLoadBalancerRouteList contains a list of NetworkLoadBalancerRoute objects. +type NetworkLoadBalancerRouteList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []NetworkLoadBalancerRoute `json:"items"` +} diff --git a/api/v1alpha/zz_generated.deepcopy.go b/api/v1alpha/zz_generated.deepcopy.go index 5572dbc4..f46f7b34 100644 --- a/api/v1alpha/zz_generated.deepcopy.go +++ b/api/v1alpha/zz_generated.deepcopy.go @@ -835,6 +835,21 @@ func (in *LocalNetworkRef) DeepCopy() *LocalNetworkRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalSecretRef) DeepCopyInto(out *LocalSecretRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalSecretRef. +func (in *LocalSecretRef) DeepCopy() *LocalSecretRef { + if in == nil { + return nil + } + out := new(LocalSecretRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LocalSubnetReference) DeepCopyInto(out *LocalSubnetReference) { *out = *in @@ -1834,6 +1849,574 @@ func (in *NetworkList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancer) DeepCopyInto(out *NetworkLoadBalancer) { + *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 NetworkLoadBalancer. +func (in *NetworkLoadBalancer) DeepCopy() *NetworkLoadBalancer { + if in == nil { + return nil + } + out := new(NetworkLoadBalancer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NetworkLoadBalancer) 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 *NetworkLoadBalancerAddressRequest) DeepCopyInto(out *NetworkLoadBalancerAddressRequest) { + *out = *in + if in.Families != nil { + in, out := &in.Families, &out.Families + *out = make([]IPFamily, len(*in)) + copy(*out, *in) + } + if in.Locations != nil { + in, out := &in.Locations, &out.Locations + *out = new(NetworkLoadBalancerLocationSelection) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerAddressRequest. +func (in *NetworkLoadBalancerAddressRequest) DeepCopy() *NetworkLoadBalancerAddressRequest { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerAddressRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerAddressRequestStatus) DeepCopyInto(out *NetworkLoadBalancerAddressRequestStatus) { + *out = *in + if in.ResolvedLocations != nil { + in, out := &in.ResolvedLocations, &out.ResolvedLocations + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerAddressRequestStatus. +func (in *NetworkLoadBalancerAddressRequestStatus) DeepCopy() *NetworkLoadBalancerAddressRequestStatus { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerAddressRequestStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerAddressStatus) DeepCopyInto(out *NetworkLoadBalancerAddressStatus) { + *out = *in + if in.ClaimRef != nil { + in, out := &in.ClaimRef, &out.ClaimRef + *out = new(NetworkLoadBalancerIPClaimRef) + **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 NetworkLoadBalancerAddressStatus. +func (in *NetworkLoadBalancerAddressStatus) DeepCopy() *NetworkLoadBalancerAddressStatus { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerAddressStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerBackendTransport) DeepCopyInto(out *NetworkLoadBalancerBackendTransport) { + *out = *in + if in.CABundleRef != nil { + in, out := &in.CABundleRef, &out.CABundleRef + *out = new(LocalSecretRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerBackendTransport. +func (in *NetworkLoadBalancerBackendTransport) DeepCopy() *NetworkLoadBalancerBackendTransport { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerBackendTransport) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerEndpointStatus) DeepCopyInto(out *NetworkLoadBalancerEndpointStatus) { + *out = *in + if in.Addresses != nil { + in, out := &in.Addresses, &out.Addresses + *out = make([]NetworkLoadBalancerAddressStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerEndpointStatus. +func (in *NetworkLoadBalancerEndpointStatus) DeepCopy() *NetworkLoadBalancerEndpointStatus { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerEndpointStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerExposure) DeepCopyInto(out *NetworkLoadBalancerExposure) { + *out = *in + if in.Addresses != nil { + in, out := &in.Addresses, &out.Addresses + *out = make([]NetworkLoadBalancerAddressRequest, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerExposure. +func (in *NetworkLoadBalancerExposure) DeepCopy() *NetworkLoadBalancerExposure { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerExposure) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerHealthCheck) DeepCopyInto(out *NetworkLoadBalancerHealthCheck) { + *out = *in + if in.IntervalSeconds != nil { + in, out := &in.IntervalSeconds, &out.IntervalSeconds + *out = new(int32) + **out = **in + } + if in.TimeoutSeconds != nil { + in, out := &in.TimeoutSeconds, &out.TimeoutSeconds + *out = new(int32) + **out = **in + } + if in.HealthyThreshold != nil { + in, out := &in.HealthyThreshold, &out.HealthyThreshold + *out = new(int32) + **out = **in + } + if in.UnhealthyThreshold != nil { + in, out := &in.UnhealthyThreshold, &out.UnhealthyThreshold + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerHealthCheck. +func (in *NetworkLoadBalancerHealthCheck) DeepCopy() *NetworkLoadBalancerHealthCheck { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerHealthCheck) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerIPClaimRef) DeepCopyInto(out *NetworkLoadBalancerIPClaimRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerIPClaimRef. +func (in *NetworkLoadBalancerIPClaimRef) DeepCopy() *NetworkLoadBalancerIPClaimRef { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerIPClaimRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerList) DeepCopyInto(out *NetworkLoadBalancerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NetworkLoadBalancer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerList. +func (in *NetworkLoadBalancerList) DeepCopy() *NetworkLoadBalancerList { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NetworkLoadBalancerList) 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 *NetworkLoadBalancerListener) DeepCopyInto(out *NetworkLoadBalancerListener) { + *out = *in + if in.AddressRefs != nil { + in, out := &in.AddressRefs, &out.AddressRefs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(NetworkLoadBalancerTLS) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerListener. +func (in *NetworkLoadBalancerListener) DeepCopy() *NetworkLoadBalancerListener { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerListener) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerLocationSelection) DeepCopyInto(out *NetworkLoadBalancerLocationSelection) { + *out = *in + if in.Names != nil { + in, out := &in.Names, &out.Names + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerLocationSelection. +func (in *NetworkLoadBalancerLocationSelection) DeepCopy() *NetworkLoadBalancerLocationSelection { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerLocationSelection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerNetworkServiceRef) DeepCopyInto(out *NetworkLoadBalancerNetworkServiceRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerNetworkServiceRef. +func (in *NetworkLoadBalancerNetworkServiceRef) DeepCopy() *NetworkLoadBalancerNetworkServiceRef { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerNetworkServiceRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerRoute) DeepCopyInto(out *NetworkLoadBalancerRoute) { + *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 NetworkLoadBalancerRoute. +func (in *NetworkLoadBalancerRoute) DeepCopy() *NetworkLoadBalancerRoute { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerRoute) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NetworkLoadBalancerRoute) 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 *NetworkLoadBalancerRouteBackend) DeepCopyInto(out *NetworkLoadBalancerRouteBackend) { + *out = *in + out.NetworkService = in.NetworkService + if in.HealthCheck != nil { + in, out := &in.HealthCheck, &out.HealthCheck + *out = new(NetworkLoadBalancerHealthCheck) + (*in).DeepCopyInto(*out) + } + if in.Transport != nil { + in, out := &in.Transport, &out.Transport + *out = new(NetworkLoadBalancerBackendTransport) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerRouteBackend. +func (in *NetworkLoadBalancerRouteBackend) DeepCopy() *NetworkLoadBalancerRouteBackend { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerRouteBackend) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerRouteList) DeepCopyInto(out *NetworkLoadBalancerRouteList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NetworkLoadBalancerRoute, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerRouteList. +func (in *NetworkLoadBalancerRouteList) DeepCopy() *NetworkLoadBalancerRouteList { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerRouteList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NetworkLoadBalancerRouteList) 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 *NetworkLoadBalancerRouteParentRef) DeepCopyInto(out *NetworkLoadBalancerRouteParentRef) { + *out = *in + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerRouteParentRef. +func (in *NetworkLoadBalancerRouteParentRef) DeepCopy() *NetworkLoadBalancerRouteParentRef { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerRouteParentRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerRouteParentStatus) DeepCopyInto(out *NetworkLoadBalancerRouteParentStatus) { + *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 NetworkLoadBalancerRouteParentStatus. +func (in *NetworkLoadBalancerRouteParentStatus) DeepCopy() *NetworkLoadBalancerRouteParentStatus { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerRouteParentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerRouteSpec) DeepCopyInto(out *NetworkLoadBalancerRouteSpec) { + *out = *in + if in.ParentRefs != nil { + in, out := &in.ParentRefs, &out.ParentRefs + *out = make([]NetworkLoadBalancerRouteParentRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Backends != nil { + in, out := &in.Backends, &out.Backends + *out = make([]NetworkLoadBalancerRouteBackend, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.SNIHostnames != nil { + in, out := &in.SNIHostnames, &out.SNIHostnames + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerRouteSpec. +func (in *NetworkLoadBalancerRouteSpec) DeepCopy() *NetworkLoadBalancerRouteSpec { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerRouteSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerRouteStatus) DeepCopyInto(out *NetworkLoadBalancerRouteStatus) { + *out = *in + if in.Parents != nil { + in, out := &in.Parents, &out.Parents + *out = make([]NetworkLoadBalancerRouteParentStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerRouteStatus. +func (in *NetworkLoadBalancerRouteStatus) DeepCopy() *NetworkLoadBalancerRouteStatus { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerRouteStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerSpec) DeepCopyInto(out *NetworkLoadBalancerSpec) { + *out = *in + out.NetworkRef = in.NetworkRef + in.Exposure.DeepCopyInto(&out.Exposure) + if in.Listeners != nil { + in, out := &in.Listeners, &out.Listeners + *out = make([]NetworkLoadBalancerListener, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerSpec. +func (in *NetworkLoadBalancerSpec) DeepCopy() *NetworkLoadBalancerSpec { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerStatus) DeepCopyInto(out *NetworkLoadBalancerStatus) { + *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]) + } + } + if in.AddressRequests != nil { + in, out := &in.AddressRequests, &out.AddressRequests + *out = make([]NetworkLoadBalancerAddressRequestStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Endpoints != nil { + in, out := &in.Endpoints, &out.Endpoints + *out = make([]NetworkLoadBalancerEndpointStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerStatus. +func (in *NetworkLoadBalancerStatus) DeepCopy() *NetworkLoadBalancerStatus { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkLoadBalancerTLS) DeepCopyInto(out *NetworkLoadBalancerTLS) { + *out = *in + if in.CertificateRef != nil { + in, out := &in.CertificateRef, &out.CertificateRef + *out = new(LocalSecretRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkLoadBalancerTLS. +func (in *NetworkLoadBalancerTLS) DeepCopy() *NetworkLoadBalancerTLS { + if in == nil { + return nil + } + out := new(NetworkLoadBalancerTLS) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkPolicy) DeepCopyInto(out *NetworkPolicy) { *out = *in diff --git a/config/crd/bases/networking.datumapis.com_networkloadbalancerroutes.yaml b/config/crd/bases/networking.datumapis.com_networkloadbalancerroutes.yaml new file mode 100644 index 00000000..7ad7a26c --- /dev/null +++ b/config/crd/bases/networking.datumapis.com_networkloadbalancerroutes.yaml @@ -0,0 +1,279 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: networkloadbalancerroutes.networking.datumapis.com +spec: + group: networking.datumapis.com + names: + kind: NetworkLoadBalancerRoute + listKind: NetworkLoadBalancerRouteList + plural: networkloadbalancerroutes + singular: networkloadbalancerroute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha + schema: + openAPIV3Schema: + description: |- + NetworkLoadBalancerRoute connects load balancer listeners to NetworkService + backends. References are restricted to the route's namespace. + 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: NetworkLoadBalancerRouteSpec attaches backends to named listeners. + properties: + backends: + items: + description: |- + NetworkLoadBalancerRouteBackend forwards new connections or flows to a + named NetworkService port. + properties: + healthCheck: + description: |- + NetworkLoadBalancerHealthCheck checks an individual backend endpoint. UDP + backends require a separate TCP, HTTP, or HTTPS health endpoint. + properties: + healthyThreshold: + format: int32 + minimum: 1 + type: integer + intervalSeconds: + format: int32 + minimum: 1 + type: integer + path: + pattern: ^/.*$ + type: string + port: + description: |- + Name of the probe port on the referenced NetworkService. When omitted, + TCP probes use the backend's serving port. + type: string + protocol: + enum: + - TCP + - HTTP + - HTTPS + type: string + timeoutSeconds: + format: int32 + minimum: 1 + type: integer + unhealthyThreshold: + format: int32 + minimum: 1 + type: integer + required: + - protocol + type: object + x-kubernetes-validations: + - message: path is only valid for HTTP and HTTPS probes + rule: 'has(self.path) ? self.protocol in [''HTTP'', ''HTTPS''] + : true' + - message: HTTP and HTTPS probes require a port + rule: self.protocol == 'TCP' || has(self.port) + networkService: + description: |- + NetworkLoadBalancerNetworkServiceRef names a backend service and one of its + named ports in the route's namespace. + properties: + name: + type: string + namespace: + type: string + port: + type: string + required: + - name + - port + type: object + transport: + description: Backend TLS requires a proxy-capable serving path. + properties: + caBundleRef: + properties: + name: + description: Name identifies a Secret in the listener's + namespace. + type: string + namespace: + description: Cross-namespace access is capability-gated + until grants are supported. + type: string + required: + - name + type: object + protocol: + enum: + - TCP + - TLS + type: string + serverName: + type: string + required: + - protocol + type: object + required: + - networkService + type: object + maxItems: 16 + minItems: 1 + type: array + parentRefs: + items: + description: |- + NetworkLoadBalancerRouteParentRef attaches a route to a listener. Cross- + namespace references are capability-gated initially. Either listener name or + port selects a listener; status always reports its resolved name. + properties: + listener: + description: One of listener or port selects the parent listener. + type: string + name: + maxLength: 253 + minLength: 1 + type: string + namespace: + type: string + port: + format: int32 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: exactly one of listener or port is required + rule: has(self.listener) != has(self.port) + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + sniHostnames: + description: SNI routing is capability-gated until TLS listeners are + served. + items: + type: string + type: array + required: + - backends + - parentRefs + type: object + status: + properties: + parents: + items: + description: |- + NetworkLoadBalancerRouteParentStatus reports attachment and effectiveness + for one parent listener. The oldest accepted catch-all route is effective. + properties: + conditions: + description: |- + Conditions include Accepted, ResolvedRefs, and Programmed with + observedGeneration. + 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 + effective: + type: boolean + listener: + type: string + name: + type: string + required: + - effective + - listener + - name + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + - listener + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/networking.datumapis.com_networkloadbalancers.yaml b/config/crd/bases/networking.datumapis.com_networkloadbalancers.yaml new file mode 100644 index 00000000..13680d0a --- /dev/null +++ b/config/crd/bases/networking.datumapis.com_networkloadbalancers.yaml @@ -0,0 +1,454 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: networkloadbalancers.networking.datumapis.com +spec: + group: networking.datumapis.com + names: + kind: NetworkLoadBalancer + listKind: NetworkLoadBalancerList + plural: networkloadbalancers + singular: networkloadbalancer + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha + schema: + openAPIV3Schema: + description: |- + NetworkLoadBalancer reserves public IPs and accepts TCP or UDP traffic on + named listeners. Route objects attach backends to those listeners. + 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: NetworkLoadBalancerSpec defines the network, public addresses, + and listeners. + properties: + exposure: + description: NetworkLoadBalancerExposure contains the load balancer's + address requests. + properties: + addresses: + items: + description: |- + NetworkLoadBalancerAddressRequest reserves one public address per selected + location, independently of whether a route has healthy backends. + properties: + families: + items: + enum: + - IPv4 + - IPv6 + type: string + maxItems: 2 + minItems: 1 + type: array + x-kubernetes-list-type: set + locations: + description: Omission follows the effective backends' network + interfaces. + properties: + from: + description: |- + NetworkLoadBalancerLocationSource selects locations by name or follows all + eligible locations as they become available. + enum: + - Backends + - All + - Names + type: string + names: + items: + type: string + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-type: set + required: + - from + type: object + x-kubernetes-validations: + - message: names must be set only when from is Names + rule: 'self.from == ''Names'' ? has(self.names) && size(self.names) + > 0 : !has(self.names)' + name: + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - families + - name + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - addresses + type: object + listeners: + items: + description: |- + NetworkLoadBalancerListener accepts one transport and port on its named + address requests. TCP and UDP may use the same port. + properties: + addressRefs: + items: + type: string + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: set + hostname: + type: string + name: + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + format: int32 + maximum: 65535 + minimum: 1 + type: integer + protocol: + description: NetworkLoadBalancerProtocol is the transport accepted + by a listener. + enum: + - TCP + - UDP + - TLS + type: string + tls: + properties: + certificateRef: + description: Secret references are admitted only after TLS + termination is supported. + properties: + name: + description: Name identifies a Secret in the listener's + namespace. + type: string + namespace: + description: Cross-namespace access is capability-gated + until grants are supported. + type: string + required: + - name + type: object + mode: + enum: + - Passthrough + - Terminate + type: string + required: + - mode + type: object + x-kubernetes-validations: + - message: Terminate requires a certificate; Passthrough omits + it + rule: 'self.mode == ''Terminate'' ? has(self.certificateRef) + : !has(self.certificateRef)' + required: + - addressRefs + - name + - port + - protocol + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + networkRef: + description: The network must exist in the load balancer's namespace. + properties: + name: + description: The network name + type: string + required: + - name + type: object + x-kubernetes-validations: + - message: networkRef is immutable + rule: self == oldSelf + required: + - exposure + - listeners + - networkRef + type: object + x-kubernetes-validations: + - message: each listener address reference must name an address request + rule: self.listeners.all(l, l.addressRefs.all(r, r in self.exposure.addresses.map(a, + a.name))) + - message: listeners sharing an address cannot use the same protocol and + port + rule: self.listeners.all(l1, self.listeners.all(l2, l1.name == l2.name + || l1.protocol != l2.protocol || l1.port != l2.port || !l1.addressRefs.exists(r, + r in l2.addressRefs))) + - message: TLS configuration is required only for TLS listeners + rule: 'self.listeners.all(l, l.protocol == ''TLS'' ? has(l.tls) : !has(l.tls))' + status: + description: |- + NetworkLoadBalancerStatus distinguishes address allocation from programming + and announcement. Allocated IPs remain here during backend failures. + properties: + addressRequests: + items: + properties: + locationSource: + description: |- + NetworkLoadBalancerLocationSource selects locations by name or follows all + eligible locations as they become available. + enum: + - Backends + - All + - Names + type: string + name: + type: string + resolvedLocations: + items: + type: string + type: array + x-kubernetes-list-type: set + required: + - locationSource + - name + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + conditions: + description: Conditions include Allocated, Programmed, and Ready with + observedGeneration. + 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 + endpoints: + items: + description: |- + NetworkLoadBalancerEndpointStatus is stable for one address request and + location, even while individual address families are unavailable. + properties: + addresses: + items: + description: NetworkLoadBalancerAddressStatus describes one + family at a location. + properties: + announced: + type: boolean + claimRef: + description: NetworkLoadBalancerIPClaimRef identifies + the claim holding an allocated IP. + properties: + name: + type: string + namespace: + type: string + project: + type: string + uid: + description: UID distinguishes a recreated claim with + the same name from the old allocation. + type: string + required: + - name + - namespace + - project + type: object + conditions: + description: |- + Conditions include Allocated, Programmed, and Announced with + observedGeneration. An entry with no value reports a pending or failed + allocation for this request/location. + 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 + dnsPublished: + type: boolean + family: + enum: + - IPv4 + - IPv6 + type: string + message: + type: string + reason: + type: string + value: + type: string + required: + - family + type: object + type: array + x-kubernetes-list-map-keys: + - family + x-kubernetes-list-type: map + canonicalHostname: + type: string + location: + type: string + requestName: + type: string + required: + - location + - requestName + type: object + maxItems: 1024 + type: array + x-kubernetes-list-map-keys: + - requestName + - location + x-kubernetes-list-type: map + 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 4e7487d9..cedb4a3a 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -18,6 +18,8 @@ resources: - bases/networking.datumapis.com_networkinterfaces.yaml - bases/networking.datumapis.com_networkinterfaceclaims.yaml - bases/networking.datumapis.com_networkservices.yaml +- bases/networking.datumapis.com_networkloadbalancers.yaml +- bases/networking.datumapis.com_networkloadbalancerroutes.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/config/quota/claim-policies/kustomization.yaml b/config/quota/claim-policies/kustomization.yaml index 85216a89..16c37530 100644 --- a/config/quota/claim-policies/kustomization.yaml +++ b/config/quota/claim-policies/kustomization.yaml @@ -16,5 +16,7 @@ resources: - httpproxy-claim-policy.yaml - httproute-claim-policy.yaml - httproutefilter-claim-policy.yaml + - networkloadbalancer-claim-policy.yaml + - networkloadbalancerroute-claim-policy.yaml - securitypolicy-claim-policy.yaml - trafficprotectionpolicy-claim-policy.yaml diff --git a/config/quota/claim-policies/networkloadbalancer-claim-policy.yaml b/config/quota/claim-policies/networkloadbalancer-claim-policy.yaml new file mode 100644 index 00000000..5f759557 --- /dev/null +++ b/config/quota/claim-policies/networkloadbalancer-claim-policy.yaml @@ -0,0 +1,26 @@ +apiVersion: quota.miloapis.com/v1alpha1 +kind: ClaimCreationPolicy +metadata: + name: project-networkloadbalancer-claim-policy + labels: + app.kubernetes.io/name: network-services-operator + app.kubernetes.io/component: quota-system +spec: + trigger: + resource: + apiVersion: networking.datumapis.com/v1alpha + kind: NetworkLoadBalancer + target: + resourceClaimTemplate: + metadata: + name: "networkloadbalancer-{{ trigger.metadata.name }}" + namespace: "{{requestInfo.namespace}}" + labels: + app.kubernetes.io/name: network-services-operator + app.kubernetes.io/component: quota-system + annotations: + kubernetes.io/description: "Automatic quota claim for NetworkLoadBalancer creation" + spec: + requests: + - resourceType: networking.datumapis.com/networkloadbalancers + amount: 1 diff --git a/config/quota/claim-policies/networkloadbalancerroute-claim-policy.yaml b/config/quota/claim-policies/networkloadbalancerroute-claim-policy.yaml new file mode 100644 index 00000000..76318b66 --- /dev/null +++ b/config/quota/claim-policies/networkloadbalancerroute-claim-policy.yaml @@ -0,0 +1,26 @@ +apiVersion: quota.miloapis.com/v1alpha1 +kind: ClaimCreationPolicy +metadata: + name: project-networkloadbalancerroute-claim-policy + labels: + app.kubernetes.io/name: network-services-operator + app.kubernetes.io/component: quota-system +spec: + trigger: + resource: + apiVersion: networking.datumapis.com/v1alpha + kind: NetworkLoadBalancerRoute + target: + resourceClaimTemplate: + metadata: + name: "networkloadbalancerroute-{{ trigger.metadata.name }}" + namespace: "{{requestInfo.namespace}}" + labels: + app.kubernetes.io/name: network-services-operator + app.kubernetes.io/component: quota-system + annotations: + kubernetes.io/description: "Automatic quota claim for NetworkLoadBalancerRoute creation" + spec: + requests: + - resourceType: networking.datumapis.com/networkloadbalancerroutes + amount: 1 diff --git a/config/quota/registrations/kustomization.yaml b/config/quota/registrations/kustomization.yaml index 7f720cd2..fa20dcb2 100644 --- a/config/quota/registrations/kustomization.yaml +++ b/config/quota/registrations/kustomization.yaml @@ -16,5 +16,7 @@ resources: - httpproxy-registration.yaml - httproute-registration.yaml - httproutefilter-registration.yaml + - networkloadbalancer-registration.yaml + - networkloadbalancerroute-registration.yaml - securitypolicy-registration.yaml - trafficprotectionpolicy-registration.yaml diff --git a/config/quota/registrations/networkloadbalancer-registration.yaml b/config/quota/registrations/networkloadbalancer-registration.yaml new file mode 100644 index 00000000..ddc1031e --- /dev/null +++ b/config/quota/registrations/networkloadbalancer-registration.yaml @@ -0,0 +1,24 @@ +apiVersion: quota.miloapis.com/v1alpha1 +kind: ResourceRegistration +metadata: + name: networkloadbalancers-per-project + labels: + app.kubernetes.io/name: network-services-operator + app.kubernetes.io/component: quota-system + services.miloapis.com/owner: networking.datumapis.com + annotations: + kubernetes.io/display-name: "Network Load Balancers" + kubernetes.io/description: "Maximum number of network load balancers in a project" +spec: + consumerType: + apiGroup: resourcemanager.miloapis.com + kind: Project + type: Entity + resourceType: networking.datumapis.com/networkloadbalancers + description: "Maximum number of network load balancers in a project" + baseUnit: load balancer + displayUnit: load balancers + unitConversionFactor: 1 + claimingResources: + - apiGroup: networking.datumapis.com + kind: NetworkLoadBalancer diff --git a/config/quota/registrations/networkloadbalancerroute-registration.yaml b/config/quota/registrations/networkloadbalancerroute-registration.yaml new file mode 100644 index 00000000..9a6109f7 --- /dev/null +++ b/config/quota/registrations/networkloadbalancerroute-registration.yaml @@ -0,0 +1,24 @@ +apiVersion: quota.miloapis.com/v1alpha1 +kind: ResourceRegistration +metadata: + name: networkloadbalancerroutes-per-project + labels: + app.kubernetes.io/name: network-services-operator + app.kubernetes.io/component: quota-system + services.miloapis.com/owner: networking.datumapis.com + annotations: + kubernetes.io/display-name: "Network Load Balancer Routes" + kubernetes.io/description: "Maximum number of network load balancer routes in a project" +spec: + consumerType: + apiGroup: resourcemanager.miloapis.com + kind: Project + type: Entity + resourceType: networking.datumapis.com/networkloadbalancerroutes + description: "Maximum number of network load balancer routes in a project" + baseUnit: route + displayUnit: routes + unitConversionFactor: 1 + claimingResources: + - apiGroup: networking.datumapis.com + kind: NetworkLoadBalancerRoute diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 8615607d..c2e8e2f8 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -190,6 +190,46 @@ webhooks: resources: - networks sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-networking-datumapis-com-v1alpha-networkloadbalancer + failurePolicy: Fail + name: vnetworkloadbalancer-v1alpha.kb.io + rules: + - apiGroups: + - networking.datumapis.com + apiVersions: + - v1alpha + operations: + - CREATE + - UPDATE + resources: + - networkloadbalancers + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-networking-datumapis-com-v1alpha-networkloadbalancerroute + failurePolicy: Fail + name: vnetworkloadbalancerroute-v1alpha.kb.io + rules: + - apiGroups: + - networking.datumapis.com + apiVersions: + - v1alpha + operations: + - CREATE + - UPDATE + resources: + - networkloadbalancerroutes + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/docs/api/networkloadbalancerroutes.md b/docs/api/networkloadbalancerroutes.md new file mode 100644 index 00000000..d22e2350 --- /dev/null +++ b/docs/api/networkloadbalancerroutes.md @@ -0,0 +1,565 @@ +# API Reference + +Packages: + +- [networking.datumapis.com/v1alpha](#networkingdatumapiscomv1alpha) + +# networking.datumapis.com/v1alpha + +Resource Types: + +- [NetworkLoadBalancerRoute](#networkloadbalancerroute) + + + + +## NetworkLoadBalancerRoute +[↩ Parent](#networkingdatumapiscomv1alpha ) + + + + + + +NetworkLoadBalancerRoute connects load balancer listeners to NetworkService +backends. References are restricted to the route's namespace. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
apiVersionstringnetworking.datumapis.com/v1alphatrue
kindstringNetworkLoadBalancerRoutetrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject + NetworkLoadBalancerRouteSpec attaches backends to named listeners.
+
true
statusobject +
+
false
+ + +### NetworkLoadBalancerRoute.spec +[↩ Parent](#networkloadbalancerroute) + + + +NetworkLoadBalancerRouteSpec attaches backends to named listeners. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
backends[]object +
+
true
parentRefs[]object +
+
true
sniHostnames[]string + SNI routing is capability-gated until TLS listeners are served.
+
false
+ + +### NetworkLoadBalancerRoute.spec.backends[index] +[↩ Parent](#networkloadbalancerroutespec) + + + +NetworkLoadBalancerRouteBackend forwards new connections or flows to a +named NetworkService port. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
networkServiceobject + NetworkLoadBalancerNetworkServiceRef names a backend service and one of its +named ports in the route's namespace.
+
true
healthCheckobject + NetworkLoadBalancerHealthCheck checks an individual backend endpoint. UDP +backends require a separate TCP, HTTP, or HTTPS health endpoint.
+
+ Validations:
  • has(self.path) ? self.protocol in ['HTTP', 'HTTPS'] : true: path is only valid for HTTP and HTTPS probes
  • self.protocol == 'TCP' || has(self.port): HTTP and HTTPS probes require a port
  • +
    false
    transportobject + Backend TLS requires a proxy-capable serving path.
    +
    false
    + + +### NetworkLoadBalancerRoute.spec.backends[index].networkService +[↩ Parent](#networkloadbalancerroutespecbackendsindex) + + + +NetworkLoadBalancerNetworkServiceRef names a backend service and one of its +named ports in the route's namespace. + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    namestring +
    +
    true
    portstring +
    +
    true
    namespacestring +
    +
    false
    + + +### NetworkLoadBalancerRoute.spec.backends[index].healthCheck +[↩ Parent](#networkloadbalancerroutespecbackendsindex) + + + +NetworkLoadBalancerHealthCheck checks an individual backend endpoint. UDP +backends require a separate TCP, HTTP, or HTTPS health endpoint. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    protocolenum +
    +
    + Enum: TCP, HTTP, HTTPS
    +
    true
    healthyThresholdinteger +
    +
    + Format: int32
    + Minimum: 1
    +
    false
    intervalSecondsinteger +
    +
    + Format: int32
    + Minimum: 1
    +
    false
    pathstring +
    +
    false
    portstring + Name of the probe port on the referenced NetworkService. When omitted, +TCP probes use the backend's serving port.
    +
    false
    timeoutSecondsinteger +
    +
    + Format: int32
    + Minimum: 1
    +
    false
    unhealthyThresholdinteger +
    +
    + Format: int32
    + Minimum: 1
    +
    false
    + + +### NetworkLoadBalancerRoute.spec.backends[index].transport +[↩ Parent](#networkloadbalancerroutespecbackendsindex) + + + +Backend TLS requires a proxy-capable serving path. + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    protocolenum +
    +
    + Enum: TCP, TLS
    +
    true
    caBundleRefobject +
    +
    false
    serverNamestring +
    +
    false
    + + +### NetworkLoadBalancerRoute.spec.backends[index].transport.caBundleRef +[↩ Parent](#networkloadbalancerroutespecbackendsindextransport) + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    namestring + Name identifies a Secret in the listener's namespace.
    +
    true
    namespacestring + Cross-namespace access is capability-gated until grants are supported.
    +
    false
    + + +### NetworkLoadBalancerRoute.spec.parentRefs[index] +[↩ Parent](#networkloadbalancerroutespec) + + + +NetworkLoadBalancerRouteParentRef attaches a route to a listener. Cross- +namespace references are capability-gated initially. Either listener name or +port selects a listener; status always reports its resolved name. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    namestring +
    +
    true
    listenerstring + One of listener or port selects the parent listener.
    +
    false
    namespacestring +
    +
    false
    portinteger +
    +
    + Format: int32
    +
    false
    + + +### NetworkLoadBalancerRoute.status +[↩ Parent](#networkloadbalancerroute) + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    parents[]object +
    +
    false
    + + +### NetworkLoadBalancerRoute.status.parents[index] +[↩ Parent](#networkloadbalancerroutestatus) + + + +NetworkLoadBalancerRouteParentStatus reports attachment and effectiveness +for one parent listener. The oldest accepted catch-all route is effective. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    effectiveboolean +
    +
    true
    listenerstring +
    +
    true
    namestring +
    +
    true
    conditions[]object + Conditions include Accepted, ResolvedRefs, and Programmed with +observedGeneration.
    +
    false
    + + +### NetworkLoadBalancerRoute.status.parents[index].conditions[index] +[↩ Parent](#networkloadbalancerroutestatusparentsindex) + + + +Condition contains details for one aspect of the current state of this API Resource. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    lastTransitionTimestring + 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
    +
    true
    messagestring + message is a human readable message indicating details about the transition. +This may be an empty string.
    +
    true
    reasonstring + 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.
    +
    true
    statusenum + status of the condition, one of True, False, Unknown.
    +
    + Enum: True, False, Unknown
    +
    true
    typestring + type of condition in CamelCase or in foo.example.com/CamelCase.
    +
    true
    observedGenerationinteger + 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
    +
    false
    diff --git a/docs/api/networkloadbalancers.md b/docs/api/networkloadbalancers.md new file mode 100644 index 00000000..f5c822ab --- /dev/null +++ b/docs/api/networkloadbalancers.md @@ -0,0 +1,809 @@ +# API Reference + +Packages: + +- [networking.datumapis.com/v1alpha](#networkingdatumapiscomv1alpha) + +# networking.datumapis.com/v1alpha + +Resource Types: + +- [NetworkLoadBalancer](#networkloadbalancer) + + + + +## NetworkLoadBalancer +[↩ Parent](#networkingdatumapiscomv1alpha ) + + + + + + +NetworkLoadBalancer reserves public IPs and accepts TCP or UDP traffic on +named listeners. Route objects attach backends to those listeners. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    apiVersionstringnetworking.datumapis.com/v1alphatrue
    kindstringNetworkLoadBalancertrue
    metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
    specobject + NetworkLoadBalancerSpec defines the network, public addresses, and listeners.
    +
    + Validations:
  • self.listeners.all(l, l.addressRefs.all(r, r in self.exposure.addresses.map(a, a.name))): each listener address reference must name an address request
  • self.listeners.all(l1, self.listeners.all(l2, l1.name == l2.name || l1.protocol != l2.protocol || l1.port != l2.port || !l1.addressRefs.exists(r, r in l2.addressRefs))): listeners sharing an address cannot use the same protocol and port
  • self.listeners.all(l, l.protocol == 'TLS' ? has(l.tls) : !has(l.tls)): TLS configuration is required only for TLS listeners
  • +
    true
    statusobject + NetworkLoadBalancerStatus distinguishes address allocation from programming +and announcement. Allocated IPs remain here during backend failures.
    +
    false
    + + +### NetworkLoadBalancer.spec +[↩ Parent](#networkloadbalancer) + + + +NetworkLoadBalancerSpec defines the network, public addresses, and listeners. + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    exposureobject + NetworkLoadBalancerExposure contains the load balancer's address requests.
    +
    true
    listeners[]object +
    +
    true
    networkRefobject + The network must exist in the load balancer's namespace.
    +
    + Validations:
  • self == oldSelf: networkRef is immutable
  • +
    true
    + + +### NetworkLoadBalancer.spec.exposure +[↩ Parent](#networkloadbalancerspec) + + + +NetworkLoadBalancerExposure contains the load balancer's address requests. + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    addresses[]object +
    +
    true
    + + +### NetworkLoadBalancer.spec.exposure.addresses[index] +[↩ Parent](#networkloadbalancerspecexposure) + + + +NetworkLoadBalancerAddressRequest reserves one public address per selected +location, independently of whether a route has healthy backends. + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    families[]enum +
    +
    + Enum: IPv4, IPv6
    +
    true
    namestring +
    +
    true
    locationsobject + Omission follows the effective backends' network interfaces.
    +
    + Validations:
  • self.from == 'Names' ? has(self.names) && size(self.names) > 0 : !has(self.names): names must be set only when from is Names
  • +
    false
    + + +### NetworkLoadBalancer.spec.exposure.addresses[index].locations +[↩ Parent](#networkloadbalancerspecexposureaddressesindex) + + + +Omission follows the effective backends' network interfaces. + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    fromenum + NetworkLoadBalancerLocationSource selects locations by name or follows all +eligible locations as they become available.
    +
    + Enum: Backends, All, Names
    +
    true
    names[]string +
    +
    false
    + + +### NetworkLoadBalancer.spec.listeners[index] +[↩ Parent](#networkloadbalancerspec) + + + +NetworkLoadBalancerListener accepts one transport and port on its named +address requests. TCP and UDP may use the same port. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    addressRefs[]string +
    +
    true
    namestring +
    +
    true
    portinteger +
    +
    + Format: int32
    + Minimum: 1
    + Maximum: 65535
    +
    true
    protocolenum + NetworkLoadBalancerProtocol is the transport accepted by a listener.
    +
    + Enum: TCP, UDP, TLS
    +
    true
    hostnamestring +
    +
    false
    tlsobject +
    +
    + Validations:
  • self.mode == 'Terminate' ? has(self.certificateRef) : !has(self.certificateRef): Terminate requires a certificate; Passthrough omits it
  • +
    false
    + + +### NetworkLoadBalancer.spec.listeners[index].tls +[↩ Parent](#networkloadbalancerspeclistenersindex) + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    modeenum +
    +
    + Enum: Passthrough, Terminate
    +
    true
    certificateRefobject + Secret references are admitted only after TLS termination is supported.
    +
    false
    + + +### NetworkLoadBalancer.spec.listeners[index].tls.certificateRef +[↩ Parent](#networkloadbalancerspeclistenersindextls) + + + +Secret references are admitted only after TLS termination is supported. + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    namestring + Name identifies a Secret in the listener's namespace.
    +
    true
    namespacestring + Cross-namespace access is capability-gated until grants are supported.
    +
    false
    + + +### NetworkLoadBalancer.spec.networkRef +[↩ Parent](#networkloadbalancerspec) + + + +The network must exist in the load balancer's namespace. + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    namestring + The network name
    +
    true
    + + +### NetworkLoadBalancer.status +[↩ Parent](#networkloadbalancer) + + + +NetworkLoadBalancerStatus distinguishes address allocation from programming +and announcement. Allocated IPs remain here during backend failures. + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    addressRequests[]object +
    +
    false
    conditions[]object + Conditions include Allocated, Programmed, and Ready with observedGeneration.
    +
    false
    endpoints[]object +
    +
    false
    + + +### NetworkLoadBalancer.status.addressRequests[index] +[↩ Parent](#networkloadbalancerstatus) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    locationSourceenum + NetworkLoadBalancerLocationSource selects locations by name or follows all +eligible locations as they become available.
    +
    + Enum: Backends, All, Names
    +
    true
    namestring +
    +
    true
    resolvedLocations[]string +
    +
    false
    + + +### NetworkLoadBalancer.status.conditions[index] +[↩ Parent](#networkloadbalancerstatus) + + + +Condition contains details for one aspect of the current state of this API Resource. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    lastTransitionTimestring + 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
    +
    true
    messagestring + message is a human readable message indicating details about the transition. +This may be an empty string.
    +
    true
    reasonstring + 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.
    +
    true
    statusenum + status of the condition, one of True, False, Unknown.
    +
    + Enum: True, False, Unknown
    +
    true
    typestring + type of condition in CamelCase or in foo.example.com/CamelCase.
    +
    true
    observedGenerationinteger + 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
    +
    false
    + + +### NetworkLoadBalancer.status.endpoints[index] +[↩ Parent](#networkloadbalancerstatus) + + + +NetworkLoadBalancerEndpointStatus is stable for one address request and +location, even while individual address families are unavailable. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    locationstring +
    +
    true
    requestNamestring +
    +
    true
    addresses[]object +
    +
    false
    canonicalHostnamestring +
    +
    false
    + + +### NetworkLoadBalancer.status.endpoints[index].addresses[index] +[↩ Parent](#networkloadbalancerstatusendpointsindex) + + + +NetworkLoadBalancerAddressStatus describes one family at a location. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    familyenum +
    +
    + Enum: IPv4, IPv6
    +
    true
    announcedboolean +
    +
    false
    claimRefobject + NetworkLoadBalancerIPClaimRef identifies the claim holding an allocated IP.
    +
    false
    conditions[]object + Conditions include Allocated, Programmed, and Announced with +observedGeneration. An entry with no value reports a pending or failed +allocation for this request/location.
    +
    false
    dnsPublishedboolean +
    +
    false
    messagestring +
    +
    false
    reasonstring +
    +
    false
    valuestring +
    +
    false
    + + +### NetworkLoadBalancer.status.endpoints[index].addresses[index].claimRef +[↩ Parent](#networkloadbalancerstatusendpointsindexaddressesindex) + + + +NetworkLoadBalancerIPClaimRef identifies the claim holding an allocated IP. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    namestring +
    +
    true
    namespacestring +
    +
    true
    projectstring +
    +
    true
    uidstring + UID distinguishes a recreated claim with the same name from the old allocation.
    +
    false
    + + +### NetworkLoadBalancer.status.endpoints[index].addresses[index].conditions[index] +[↩ Parent](#networkloadbalancerstatusendpointsindexaddressesindex) + + + +Condition contains details for one aspect of the current state of this API Resource. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    lastTransitionTimestring + 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
    +
    true
    messagestring + message is a human readable message indicating details about the transition. +This may be an empty string.
    +
    true
    reasonstring + 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.
    +
    true
    statusenum + status of the condition, one of True, False, Unknown.
    +
    + Enum: True, False, Unknown
    +
    true
    typestring + type of condition in CamelCase or in foo.example.com/CamelCase.
    +
    true
    observedGenerationinteger + 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
    +
    false
    diff --git a/internal/agent/catalog_test.go b/internal/agent/catalog_test.go index 09541e37..9304e31a 100644 --- a/internal/agent/catalog_test.go +++ b/internal/agent/catalog_test.go @@ -32,20 +32,22 @@ var inScopeAPIFiles = []string{ // rather than ignored so that "scoped" cannot quietly become "stale": a new // file has to be argued into one list or the other. var outOfScopeAPIFiles = map[string]string{ - "edgereachability_types.go": "hub-side record of which addresses an edge should reach; no customer-facing status", - "groupversion_info.go": "scheme registration, no reasons", - "internaldns_types.go": "private resolver access is a network surface, not load balancer status", - "locationbinding_types.go": "placement plumbing behind a location, never named by a load balancer", - "network_types.go": "the network a service sits on; its own product surface", - "networkbinding_types.go": "network attachment plumbing", - "networkcontext_types.go": "network attachment plumbing", - "networkinterface_types.go": "an instance's interface; compute's surface", - "networkinterfaceclaim_types.go": "an instance's interface; compute's surface", - "networkpolicy_types.go": "its own product surface, not reached through a load balancer", - "subnet_types.go": "address management below a network", - "subnetclaim_types.go": "address management below a network", - "trafficprotectionpolicy_types.go": "declares no reasons of its own; the controller writes upstream Gateway API policy reasons", - "zz_generated.deepcopy.go": "generated", + "edgereachability_types.go": "hub-side record of which addresses an edge should reach; no customer-facing status", + "groupversion_info.go": "scheme registration, no reasons", + "internaldns_types.go": "private resolver access is a network surface, not load balancer status", + "locationbinding_types.go": "placement plumbing behind a location, never named by a load balancer", + "network_types.go": "the network a service sits on; its own product surface", + "networkbinding_types.go": "network attachment plumbing", + "networkcontext_types.go": "network attachment plumbing", + "networkinterface_types.go": "an instance's interface; compute's surface", + "networkinterfaceclaim_types.go": "an instance's interface; compute's surface", + "networkloadbalancer_types.go": "its own Layer 4 product surface, not reached through an application load balancer", + "networkloadbalancerroute_types.go": "its own Layer 4 routing surface, not reached through an application load balancer", + "networkpolicy_types.go": "its own product surface, not reached through a load balancer", + "subnet_types.go": "address management below a network", + "subnetclaim_types.go": "address management below a network", + "trafficprotectionpolicy_types.go": "declares no reasons of its own; the controller writes upstream Gateway API policy reasons", + "zz_generated.deepcopy.go": "generated", } // extraReasonConsts are reason constants whose NAME does not contain "Reason", diff --git a/internal/cmd/manager/manager.go b/internal/cmd/manager/manager.go index 954b6935..2b6d9fb0 100644 --- a/internal/cmd/manager/manager.go +++ b/internal/cmd/manager/manager.go @@ -605,6 +605,12 @@ func webhookRegistrations(mgr mcmanager.Manager, serverConfig config.NetworkServ {"Network", true, func() error { return networkingv1alphawebhooks.SetupNetworkWebhookWithManager(mgr) }}, + {"NetworkLoadBalancer", true, func() error { + return networkingv1alphawebhooks.SetupNetworkLoadBalancerWebhookWithManager(mgr) + }}, + {"NetworkLoadBalancerRoute", true, func() error { + return networkingv1alphawebhooks.SetupNetworkLoadBalancerRouteWebhookWithManager(mgr) + }}, {"BackendTrafficPolicy", true, func() error { return webhookgatewayv1alpha1.SetupBackendTrafficPolicyWebhookWithManager(mgr, serverConfig) }}, diff --git a/internal/cmd/manager/manager_test.go b/internal/cmd/manager/manager_test.go index 2c539391..c9e0ea6c 100644 --- a/internal/cmd/manager/manager_test.go +++ b/internal/cmd/manager/manager_test.go @@ -168,6 +168,8 @@ func TestWebhookRegistrations_RegistersEveryWebhook(t *testing.T) { "HTTPRoute", "HTTPRouteFilter", "Network", + "NetworkLoadBalancer", + "NetworkLoadBalancerRoute", "SecurityPolicy", "TrafficProtectionPolicy", } diff --git a/internal/networkagent/catalog_test.go b/internal/networkagent/catalog_test.go index c756fb1b..26796cb9 100644 --- a/internal/networkagent/catalog_test.go +++ b/internal/networkagent/catalog_test.go @@ -30,15 +30,17 @@ var inScopeAPIFiles = []string{ } var outOfScopeAPIFiles = map[string]string{ - "domain_types.go": "load balancer hostnames; the ALB agent's", - "edgereachability_types.go": "hub-side record with no customer-facing status", - "groupversion_info.go": "scheme registration, no reasons", - "httpproxy_types.go": "load balancers; the ALB agent's", - "internaldns_types.go": "internal DNS service configuration, no status reasons", - "locationbinding_types.go": "which locations a project is offered; surfaces here only as LocationNotAvailable", - "networkpolicy_types.go": "declares no status reasons", - "trafficprotectionpolicy_types.go": "load balancer protection; the ALB agent's", - "zz_generated.deepcopy.go": "generated", + "domain_types.go": "load balancer hostnames; the ALB agent's", + "edgereachability_types.go": "hub-side record with no customer-facing status", + "groupversion_info.go": "scheme registration, no reasons", + "httpproxy_types.go": "load balancers; the ALB agent's", + "internaldns_types.go": "internal DNS service configuration, no status reasons", + "locationbinding_types.go": "which locations a project is offered; surfaces here only as LocationNotAvailable", + "networkloadbalancer_types.go": "Network Load Balancer has its own product status, separate from networks and network services", + "networkloadbalancerroute_types.go": "Network Load Balancer route status belongs to the load balancer product", + "networkpolicy_types.go": "declares no status reasons", + "trafficprotectionpolicy_types.go": "load balancer protection; the ALB agent's", + "zz_generated.deepcopy.go": "generated", } var controllerFiles = []string{ diff --git a/internal/webhook/v1alpha/networkloadbalancer_webhook.go b/internal/webhook/v1alpha/networkloadbalancer_webhook.go new file mode 100644 index 00000000..9976c850 --- /dev/null +++ b/internal/webhook/v1alpha/networkloadbalancer_webhook.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha + +import ( + "context" + "slices" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha" +) + +// +kubebuilder:webhook:path=/validate-networking-datumapis-com-v1alpha-networkloadbalancer,mutating=false,failurePolicy=fail,sideEffects=None,groups=networking.datumapis.com,resources=networkloadbalancers,verbs=create;update,versions=v1alpha,name=vnetworkloadbalancer-v1alpha.kb.io,admissionReviewVersions=v1 + +type NetworkLoadBalancerValidator struct{} + +var _ admission.Validator[*networkingv1alpha.NetworkLoadBalancer] = &NetworkLoadBalancerValidator{} + +func SetupNetworkLoadBalancerWebhookWithManager(mgr mcmanager.Manager) error { + return ctrl.NewWebhookManagedBy(mgr.GetLocalManager(), &networkingv1alpha.NetworkLoadBalancer{}). + WithValidator(&NetworkLoadBalancerValidator{}).Complete() +} + +func (v *NetworkLoadBalancerValidator) ValidateCreate(_ context.Context, lb *networkingv1alpha.NetworkLoadBalancer) (admission.Warnings, error) { + return nil, validateNetworkLoadBalancerCapabilities(lb) +} + +func (v *NetworkLoadBalancerValidator) ValidateUpdate(_ context.Context, _, lb *networkingv1alpha.NetworkLoadBalancer) (admission.Warnings, error) { + return nil, validateNetworkLoadBalancerCapabilities(lb) +} + +func (v *NetworkLoadBalancerValidator) ValidateDelete(context.Context, *networkingv1alpha.NetworkLoadBalancer) (admission.Warnings, error) { + return nil, nil +} + +func validateNetworkLoadBalancerCapabilities(lb *networkingv1alpha.NetworkLoadBalancer) error { + var errs field.ErrorList + for i, request := range lb.Spec.Exposure.Addresses { + if slices.Contains(request.Families, networkingv1alpha.IPv4Protocol) { + errs = append(errs, field.Forbidden(field.NewPath("spec", "exposure", "addresses").Index(i).Child("families"), + "UnsupportedCapability: public IPv4 NLB ingress is unavailable")) + } + } + for i, listener := range lb.Spec.Listeners { + path := field.NewPath("spec", "listeners").Index(i) + if len(listener.AddressRefs) > 1 { + errs = append(errs, field.Forbidden(path.Child("addressRefs"), + "UnsupportedCapability: a listener can use only one address request until multi-VIP serving is supported")) + } + if listener.Protocol == networkingv1alpha.NetworkLoadBalancerProtocolTLS || listener.TLS != nil || listener.Hostname != "" { + errs = append(errs, field.Forbidden(path, "UnsupportedCapability: TLS listeners require a proxy-capable edge path")) + } + for _, addressName := range listener.AddressRefs { + if slices.ContainsFunc(lb.Spec.Listeners[:i], func(other networkingv1alpha.NetworkLoadBalancerListener) bool { + return slices.Contains(other.AddressRefs, addressName) + }) { + errs = append(errs, field.Forbidden(path.Child("addressRefs"), + "UnsupportedCapability: one address request cannot be shared by multiple listeners yet")) + } + } + } + if len(errs) != 0 { + return apierrors.NewInvalid(schema.GroupKind{Group: networkingv1alpha.GroupVersion.Group, Kind: "NetworkLoadBalancer"}, lb.Name, errs) + } + return nil +} diff --git a/internal/webhook/v1alpha/networkloadbalancer_webhook_test.go b/internal/webhook/v1alpha/networkloadbalancer_webhook_test.go new file mode 100644 index 00000000..6fb66610 --- /dev/null +++ b/internal/webhook/v1alpha/networkloadbalancer_webhook_test.go @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha + +import ( + "testing" + + "github.com/stretchr/testify/require" + + networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha" +) + +func TestNetworkLoadBalancerCapabilityValidation(t *testing.T) { + lb := &networkingv1alpha.NetworkLoadBalancer{} + lb.Spec.Exposure.Addresses = []networkingv1alpha.NetworkLoadBalancerAddressRequest{{ + Name: "regional", Families: []networkingv1alpha.IPFamily{networkingv1alpha.IPv6Protocol}, + }} + lb.Spec.Listeners = []networkingv1alpha.NetworkLoadBalancerListener{{ + Name: "game", Protocol: networkingv1alpha.NetworkLoadBalancerProtocolUDP, AddressRefs: []string{"regional"}, + }} + require.NoError(t, validateNetworkLoadBalancerCapabilities(lb)) + lb.Spec.Listeners[0].AddressRefs = []string{"regional", "second"} + require.ErrorContains(t, validateNetworkLoadBalancerCapabilities(lb), "multi-VIP serving") + lb.Spec.Listeners[0].AddressRefs = []string{"regional"} + lb.Spec.Exposure.Addresses[0].Families = append(lb.Spec.Exposure.Addresses[0].Families, networkingv1alpha.IPv4Protocol) + require.ErrorContains(t, validateNetworkLoadBalancerCapabilities(lb), "UnsupportedCapability") + lb.Spec.Exposure.Addresses[0].Families = []networkingv1alpha.IPFamily{networkingv1alpha.IPv6Protocol} + lb.Spec.Listeners[0].Protocol = networkingv1alpha.NetworkLoadBalancerProtocolTLS + require.ErrorContains(t, validateNetworkLoadBalancerCapabilities(lb), "proxy-capable") + lb.Spec.Listeners[0].Protocol = networkingv1alpha.NetworkLoadBalancerProtocolTCP + lb.Spec.Listeners = append(lb.Spec.Listeners, networkingv1alpha.NetworkLoadBalancerListener{ + Name: "other", Protocol: networkingv1alpha.NetworkLoadBalancerProtocolUDP, AddressRefs: []string{"regional"}, + }) + require.ErrorContains(t, validateNetworkLoadBalancerCapabilities(lb), "shared") +} + +func TestNetworkLoadBalancerRouteCapabilityValidation(t *testing.T) { + route := &networkingv1alpha.NetworkLoadBalancerRoute{} + route.Spec.Backends = []networkingv1alpha.NetworkLoadBalancerRouteBackend{{ + NetworkService: networkingv1alpha.NetworkLoadBalancerNetworkServiceRef{Name: "game", Port: "game"}, + }} + require.NoError(t, validateNetworkLoadBalancerRouteCapabilities(route)) + route.Spec.Backends = append(route.Spec.Backends, route.Spec.Backends[0]) + require.ErrorContains(t, validateNetworkLoadBalancerRouteCapabilities(route), "multiple backends") + route.Spec.Backends = route.Spec.Backends[:1] + route.Spec.Backends[0].Transport = &networkingv1alpha.NetworkLoadBalancerBackendTransport{Protocol: "TLS"} + require.ErrorContains(t, validateNetworkLoadBalancerRouteCapabilities(route), "backend TLS") +} + +func TestNetworkLoadBalancerRouteRejectsDuplicateParents(t *testing.T) { + route := &networkingv1alpha.NetworkLoadBalancerRoute{} + route.Namespace = "games" + route.Spec.ParentRefs = []networkingv1alpha.NetworkLoadBalancerRouteParentRef{ + {Name: "edge", Listener: "tcp"}, + {Name: "edge", Listener: "tcp", Namespace: "games"}, + } + require.ErrorContains(t, validateNetworkLoadBalancerRouteCapabilities(route), "Duplicate value") + route.Spec.ParentRefs[1].Listener = "udp" + require.NoError(t, validateNetworkLoadBalancerRouteCapabilities(route)) +} diff --git a/internal/webhook/v1alpha/networkloadbalancerroute_webhook.go b/internal/webhook/v1alpha/networkloadbalancerroute_webhook.go new file mode 100644 index 00000000..aa96fdcf --- /dev/null +++ b/internal/webhook/v1alpha/networkloadbalancerroute_webhook.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha" +) + +// +kubebuilder:webhook:path=/validate-networking-datumapis-com-v1alpha-networkloadbalancerroute,mutating=false,failurePolicy=fail,sideEffects=None,groups=networking.datumapis.com,resources=networkloadbalancerroutes,verbs=create;update,versions=v1alpha,name=vnetworkloadbalancerroute-v1alpha.kb.io,admissionReviewVersions=v1 + +type NetworkLoadBalancerRouteValidator struct{} + +var _ admission.Validator[*networkingv1alpha.NetworkLoadBalancerRoute] = &NetworkLoadBalancerRouteValidator{} + +func SetupNetworkLoadBalancerRouteWebhookWithManager(mgr mcmanager.Manager) error { + return ctrl.NewWebhookManagedBy(mgr.GetLocalManager(), &networkingv1alpha.NetworkLoadBalancerRoute{}). + WithValidator(&NetworkLoadBalancerRouteValidator{}).Complete() +} + +func (v *NetworkLoadBalancerRouteValidator) ValidateCreate(_ context.Context, route *networkingv1alpha.NetworkLoadBalancerRoute) (admission.Warnings, error) { + return nil, validateNetworkLoadBalancerRouteCapabilities(route) +} + +func (v *NetworkLoadBalancerRouteValidator) ValidateUpdate(_ context.Context, _, route *networkingv1alpha.NetworkLoadBalancerRoute) (admission.Warnings, error) { + return nil, validateNetworkLoadBalancerRouteCapabilities(route) +} + +func (v *NetworkLoadBalancerRouteValidator) ValidateDelete(context.Context, *networkingv1alpha.NetworkLoadBalancerRoute) (admission.Warnings, error) { + return nil, nil +} + +func validateNetworkLoadBalancerRouteCapabilities(route *networkingv1alpha.NetworkLoadBalancerRoute) error { + var errs field.ErrorList + seenParents := make(map[string]struct{}, len(route.Spec.ParentRefs)) + if len(route.Spec.Backends) > 1 { + errs = append(errs, field.Forbidden(field.NewPath("spec", "backends"), + "UnsupportedCapability: multiple backends require complete endpoint selection in serving cells")) + } + if len(route.Spec.SNIHostnames) > 0 { + errs = append(errs, field.Forbidden(field.NewPath("spec", "sniHostnames"), + "UnsupportedCapability: SNI routing requires a proxy-capable edge path")) + } + for i, parent := range route.Spec.ParentRefs { + namespace := parent.Namespace + if namespace == "" { + namespace = route.Namespace + } + selector := "listener:" + parent.Listener + if parent.Port != nil { + selector = fmt.Sprintf("port:%d", *parent.Port) + } + key := namespace + "/" + parent.Name + "/" + selector + if _, exists := seenParents[key]; exists { + errs = append(errs, field.Duplicate(field.NewPath("spec", "parentRefs").Index(i), parent)) + } + seenParents[key] = struct{}{} + if parent.Namespace != "" && parent.Namespace != route.Namespace { + errs = append(errs, field.Forbidden(field.NewPath("spec", "parentRefs").Index(i).Child("namespace"), + "UnsupportedCapability: cross-namespace parent references require grants")) + } + } + for i, backend := range route.Spec.Backends { + path := field.NewPath("spec", "backends").Index(i) + if backend.Transport != nil { + errs = append(errs, field.Forbidden(path.Child("transport"), + "UnsupportedCapability: backend TLS transport requires a proxy-capable edge path")) + } + if backend.NetworkService.Namespace != "" && backend.NetworkService.Namespace != route.Namespace { + errs = append(errs, field.Forbidden(path.Child("networkService", "namespace"), + "UnsupportedCapability: cross-namespace backends require grants")) + } + } + if len(errs) != 0 { + return apierrors.NewInvalid(schema.GroupKind{Group: networkingv1alpha.GroupVersion.Group, Kind: "NetworkLoadBalancerRoute"}, route.Name, errs) + } + return nil +}