From aa9cc1a9289b42087c473488236b36478bd8ea90 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 30 Sep 2026 15:05:51 -0500 Subject: [PATCH 1/7] feat(api): add service route policy resources --- api/v1alpha1/serviceroute_types.go | 149 ++++++++++++ api/v1alpha1/serviceroute_types_deepcopy.go | 141 +++++++++++ api/v1alpha1/serviceroute_types_test.go | 50 ++++ config/crd/kustomization.yaml | 2 + ...etwork.datumapis.com_serviceendpoints.yaml | 97 ++++++++ ...rk.datumapis.com_serviceroutepolicies.yaml | 228 ++++++++++++++++++ 6 files changed, 667 insertions(+) create mode 100644 api/v1alpha1/serviceroute_types.go create mode 100644 api/v1alpha1/serviceroute_types_deepcopy.go create mode 100644 api/v1alpha1/serviceroute_types_test.go create mode 100644 config/crd/network.datumapis.com_serviceendpoints.yaml create mode 100644 config/crd/network.datumapis.com_serviceroutepolicies.yaml diff --git a/api/v1alpha1/serviceroute_types.go b/api/v1alpha1/serviceroute_types.go new file mode 100644 index 0000000..bd38b24 --- /dev/null +++ b/api/v1alpha1/serviceroute_types.go @@ -0,0 +1,149 @@ +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ServiceEndpoint publishes a platform service that can be reached through a +// VPC attachment. The object describes the service contract; the controller +// that owns the service is responsible for its health and implementation. +// +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Namespaced,shortName=svcendpoint +// +kubebuilder:printcolumn:name="ADDRESS",type="string",JSONPath=".spec.address" +// +kubebuilder:printcolumn:name="PORT",type="integer",JSONPath=".spec.port" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +type ServiceEndpoint struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServiceEndpointSpec `json:"spec,omitempty"` +} + +// ServiceEndpointSpec defines a platform service address and its scope. +type ServiceEndpointSpec struct { + // ServiceClass identifies the platform capability implemented by this + // endpoint, for example "ipv6-egress-dns". + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + ServiceClass string `json:"serviceClass"` + + // Address is the stable address that consumers use to reach the service. + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="isIP(self)",message="address must be an IP address" + Address string `json:"address"` + + // Port is the service port. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + Port int32 `json:"port"` + + // Protocol is the transport protocol accepted by the endpoint. + // +kubebuilder:validation:Required + Protocol NetworkRuleProtocol `json:"protocol"` + + // Region limits endpoint selection to a region. An empty value means the + // endpoint is not region-scoped. + // +optional + Region string `json:"region,omitempty"` +} + +// ServiceEndpointList is a list of ServiceEndpoint resources. +// +kubebuilder:object:root=true +type ServiceEndpointList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ServiceEndpoint `json:"items"` +} + +// ServiceRoutePolicy selects VPC attachments that should receive a route to a +// platform ServiceEndpoint. The selector is evaluated by the network control +// plane; Galactic compiles the selected attachments into dataplane state. +// +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=svcroute +// +kubebuilder:printcolumn:name="SERVICE",type="string",JSONPath=".spec.serviceRef.name" +// +kubebuilder:printcolumn:name="REGION",type="string",JSONPath=".spec.region" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +type ServiceRoutePolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServiceRoutePolicySpec `json:"spec,omitempty"` + Status ServiceRoutePolicyStatus `json:"status,omitempty"` +} + +// ServiceRoutePolicySpec defines service selection and attachment eligibility. +type ServiceRoutePolicySpec struct { + // ServiceRef identifies the ServiceEndpoint in this namespace. + // +kubebuilder:validation:Required + ServiceRef ServiceEndpointReference `json:"serviceRef"` + + // AttachmentSelector selects eligible VPC attachments by authoritative + // labels. Labels that grant access to platform services must be assigned by + // the network control plane, not by tenants. + // +kubebuilder:validation:Required + AttachmentSelector metav1.LabelSelector `json:"attachmentSelector"` + + // ProtocolPorts limits the traffic that the route is intended to carry. + // An empty list means the endpoint's declared port and protocol are used. + // +optional + // +listType=atomic + ProtocolPorts []ServiceRouteProtocolPort `json:"protocolPorts,omitempty"` + + // Region limits selected attachments to a region. An empty value means the + // policy applies in every region where the endpoint is available. + // +optional + Region string `json:"region,omitempty"` +} + +// ServiceRouteProtocolPort identifies an allowed transport protocol and port. +type ServiceRouteProtocolPort struct { + // Protocol is the transport protocol. + // +kubebuilder:validation:Required + Protocol NetworkRuleProtocol `json:"protocol"` + + // Port is the transport port. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + Port int32 `json:"port"` +} + +// ServiceEndpointReference identifies a ServiceEndpoint in the policy's +// namespace. +type ServiceEndpointReference struct { + // Name is the ServiceEndpoint name. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` +} + +// ServiceRoutePolicyStatus contains durable policy conditions only. Per- +// attachment selection and programming state belongs in metrics and logs. +type ServiceRoutePolicyStatus struct { + // ObservedGeneration is the .metadata.generation this status was computed from. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Conditions contains durable acceptance and service-availability state. + // + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// ServiceRoutePolicyList is a list of ServiceRoutePolicy resources. +// +kubebuilder:object:root=true +type ServiceRoutePolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ServiceRoutePolicy `json:"items"` +} + +func init() { + SchemeBuilder.Register( + &ServiceEndpoint{}, &ServiceEndpointList{}, + &ServiceRoutePolicy{}, &ServiceRoutePolicyList{}, + ) +} diff --git a/api/v1alpha1/serviceroute_types_deepcopy.go b/api/v1alpha1/serviceroute_types_deepcopy.go new file mode 100644 index 0000000..5b851f5 --- /dev/null +++ b/api/v1alpha1/serviceroute_types_deepcopy.go @@ -0,0 +1,141 @@ +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func (in *ServiceEndpoint) DeepCopyInto(out *ServiceEndpoint) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) +} + +func (in *ServiceEndpoint) DeepCopy() *ServiceEndpoint { + if in == nil { + return nil + } + out := new(ServiceEndpoint) + in.DeepCopyInto(out) + return out +} + +func (in *ServiceEndpoint) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +func (in *ServiceEndpointList) DeepCopyInto(out *ServiceEndpointList) { + *out = *in + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServiceEndpoint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +func (in *ServiceEndpointList) DeepCopy() *ServiceEndpointList { + if in == nil { + return nil + } + out := new(ServiceEndpointList) + in.DeepCopyInto(out) + return out +} + +func (in *ServiceEndpointList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +func (in *ServiceRoutePolicy) DeepCopyInto(out *ServiceRoutePolicy) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +func (in *ServiceRoutePolicy) DeepCopy() *ServiceRoutePolicy { + if in == nil { + return nil + } + out := new(ServiceRoutePolicy) + in.DeepCopyInto(out) + return out +} + +func (in *ServiceRoutePolicy) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +func (in *ServiceRoutePolicyList) DeepCopyInto(out *ServiceRoutePolicyList) { + *out = *in + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServiceRoutePolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +func (in *ServiceRoutePolicyList) DeepCopy() *ServiceRoutePolicyList { + if in == nil { + return nil + } + out := new(ServiceRoutePolicyList) + in.DeepCopyInto(out) + return out +} + +func (in *ServiceRoutePolicyList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +func (in *ServiceRoutePolicySpec) DeepCopyInto(out *ServiceRoutePolicySpec) { + *out = *in + in.ServiceRef.DeepCopyInto(&out.ServiceRef) + in.AttachmentSelector.DeepCopyInto(&out.AttachmentSelector) + if in.ProtocolPorts != nil { + in, out := &in.ProtocolPorts, &out.ProtocolPorts + *out = make([]ServiceRouteProtocolPort, len(*in)) + copy(*out, *in) + } +} + +func (in *ServiceRoutePolicyStatus) DeepCopyInto(out *ServiceRoutePolicyStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + copy(*out, *in) + } +} + +func (in *ServiceEndpointReference) DeepCopyInto(out *ServiceEndpointReference) { + *out = *in +} + +func (in *ServiceEndpointReference) DeepCopy() *ServiceEndpointReference { + if in == nil { + return nil + } + out := new(ServiceEndpointReference) + in.DeepCopyInto(out) + return out +} diff --git a/api/v1alpha1/serviceroute_types_test.go b/api/v1alpha1/serviceroute_types_test.go new file mode 100644 index 0000000..5af2aa8 --- /dev/null +++ b/api/v1alpha1/serviceroute_types_test.go @@ -0,0 +1,50 @@ +package v1alpha1 + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestServiceRoutePolicyDeepCopy(t *testing.T) { + original := &ServiceRoutePolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "dns", Namespace: "platform"}, + Spec: ServiceRoutePolicySpec{ + ServiceRef: ServiceEndpointReference{Name: "ipv6-egress-dns"}, + AttachmentSelector: metav1.LabelSelector{MatchLabels: map[string]string{"egress": "enabled"}}, + ProtocolPorts: []ServiceRouteProtocolPort{{Protocol: NetworkRuleProtocolUDP, Port: 53}}, + Region: "us-central-1", + }, + Status: ServiceRoutePolicyStatus{Conditions: []metav1.Condition{{Type: "Accepted"}}}, + } + + copy := original.DeepCopy() + copy.Spec.AttachmentSelector.MatchLabels["egress"] = "disabled" + copy.Spec.ProtocolPorts[0].Port = 853 + copy.Status.Conditions[0].Type = "Rejected" + + if original.Spec.AttachmentSelector.MatchLabels["egress"] != "enabled" { + t.Fatal("DeepCopy shared selector labels") + } + if original.Status.Conditions[0].Type != "Accepted" { + t.Fatal("DeepCopy shared status conditions") + } + if original.Spec.ProtocolPorts[0].Port != 53 { + t.Fatal("DeepCopy shared protocol ports") + } +} + +func TestServiceRoutePolicyListDeepCopy(t *testing.T) { + original := &ServiceRoutePolicyList{Items: []ServiceRoutePolicy{{ + Spec: ServiceRoutePolicySpec{ + AttachmentSelector: metav1.LabelSelector{MatchExpressions: []metav1.LabelSelectorRequirement{{ + Key: "region", Operator: metav1.LabelSelectorOpIn, Values: []string{"us-central-1"}, + }}}, + }}}} + + copy := original.DeepCopy() + copy.Items[0].Spec.AttachmentSelector.MatchExpressions[0].Values[0] = "eu-west-1" + if got := original.Items[0].Spec.AttachmentSelector.MatchExpressions[0].Values[0]; got != "us-central-1" { + t.Fatalf("DeepCopy shared selector values: got %q", got) + } +} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index ea068d4..c8fc66f 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -12,3 +12,5 @@ resources: - network.datumapis.com_networkgateways.yaml - network.datumapis.com_networkrules.yaml - network.datumapis.com_servicevipbindings.yaml + - network.datumapis.com_serviceendpoints.yaml + - network.datumapis.com_serviceroutepolicies.yaml diff --git a/config/crd/network.datumapis.com_serviceendpoints.yaml b/config/crd/network.datumapis.com_serviceendpoints.yaml new file mode 100644 index 0000000..2fb9970 --- /dev/null +++ b/config/crd/network.datumapis.com_serviceendpoints.yaml @@ -0,0 +1,97 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: serviceendpoints.network.datumapis.com +spec: + group: network.datumapis.com + names: + kind: ServiceEndpoint + listKind: ServiceEndpointList + plural: serviceendpoints + shortNames: + - svcendpoint + singular: serviceendpoint + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.address + name: ADDRESS + type: string + - jsonPath: .spec.port + name: PORT + type: integer + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + ServiceEndpoint publishes a platform service that can be reached through a + VPC attachment. The object describes the service contract; the controller + that owns the service is responsible for its health and implementation. + 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: ServiceEndpointSpec defines a platform service address and + its scope. + properties: + address: + description: Address is the stable address that consumers use to reach + the service. + type: string + x-kubernetes-validations: + - message: address must be an IP address + rule: isIP(self) + port: + description: Port is the service port. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + protocol: + description: Protocol is the transport protocol accepted by the endpoint. + enum: + - tcp + - udp + type: string + region: + description: |- + Region limits endpoint selection to a region. An empty value means the + endpoint is not region-scoped. + type: string + serviceClass: + description: |- + ServiceClass identifies the platform capability implemented by this + endpoint, for example "ipv6-egress-dns". + minLength: 1 + type: string + required: + - address + - port + - protocol + - serviceClass + type: object + type: object + served: true + storage: true + subresources: {} diff --git a/config/crd/network.datumapis.com_serviceroutepolicies.yaml b/config/crd/network.datumapis.com_serviceroutepolicies.yaml new file mode 100644 index 0000000..67d33c3 --- /dev/null +++ b/config/crd/network.datumapis.com_serviceroutepolicies.yaml @@ -0,0 +1,228 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: serviceroutepolicies.network.datumapis.com +spec: + group: network.datumapis.com + names: + kind: ServiceRoutePolicy + listKind: ServiceRoutePolicyList + plural: serviceroutepolicies + shortNames: + - svcroute + singular: serviceroutepolicy + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.serviceRef.name + name: SERVICE + type: string + - jsonPath: .spec.region + name: REGION + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + ServiceRoutePolicy selects VPC attachments that should receive a route to a + platform ServiceEndpoint. The selector is evaluated by the network control + plane; Galactic compiles the selected attachments into dataplane state. + 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: ServiceRoutePolicySpec defines service selection and attachment + eligibility. + properties: + attachmentSelector: + description: |- + AttachmentSelector selects eligible VPC attachments by authoritative + labels. Labels that grant access to platform services must be assigned by + the network control plane, not by tenants. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + protocolPorts: + description: |- + ProtocolPorts limits the traffic that the route is intended to carry. + An empty list means the endpoint's declared port and protocol are used. + items: + description: ServiceRouteProtocolPort identifies an allowed transport + protocol and port. + properties: + port: + description: Port is the transport port. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + protocol: + description: Protocol is the transport protocol. + enum: + - tcp + - udp + type: string + required: + - port + - protocol + type: object + type: array + x-kubernetes-list-type: atomic + region: + description: |- + Region limits selected attachments to a region. An empty value means the + policy applies in every region where the endpoint is available. + type: string + serviceRef: + description: ServiceRef identifies the ServiceEndpoint in this namespace. + properties: + name: + description: Name is the ServiceEndpoint name. + minLength: 1 + type: string + required: + - name + type: object + required: + - attachmentSelector + - serviceRef + type: object + status: + description: |- + ServiceRoutePolicyStatus contains durable policy conditions only. Per- + attachment selection and programming state belongs in metrics and logs. + properties: + conditions: + description: Conditions contains durable acceptance and service-availability + state. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + description: ObservedGeneration is the .metadata.generation this status + was computed from. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} From befe52dcd5e538a8bb1c80861dfd41c47f9a11f9 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 30 Sep 2026 16:06:53 -0500 Subject: [PATCH 2/7] feat(api): reference service endpoint attachment --- api/v1alpha1/serviceroute_types.go | 19 ++++++++++++++++ api/v1alpha1/serviceroute_types_deepcopy.go | 18 +++++++++++++++ ...etwork.datumapis.com_serviceendpoints.yaml | 22 ++++++++++++++++++- 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/serviceroute_types.go b/api/v1alpha1/serviceroute_types.go index bd38b24..c931051 100644 --- a/api/v1alpha1/serviceroute_types.go +++ b/api/v1alpha1/serviceroute_types.go @@ -41,12 +41,31 @@ type ServiceEndpointSpec struct { // +kubebuilder:validation:Required Protocol NetworkRuleProtocol `json:"protocol"` + // AttachmentRef identifies the Cloud API VPCAttachment that hosts this + // endpoint when the service is delivered through a private VPC path. + // The network API keeps this reference opaque; the consuming controller + // resolves it against the Cloud API. + // +optional + AttachmentRef *ServiceEndpointAttachmentReference `json:"attachmentRef,omitempty"` + // Region limits endpoint selection to a region. An empty value means the // endpoint is not region-scoped. // +optional Region string `json:"region,omitempty"` } +// ServiceEndpointAttachmentReference identifies the private attachment that +// backs a platform service endpoint. +type ServiceEndpointAttachmentReference struct { + // Namespace is the namespace containing the Cloud API VPCAttachment. + // +kubebuilder:validation:MinLength=1 + Namespace string `json:"namespace"` + + // Name is the Cloud API VPCAttachment name. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` +} + // ServiceEndpointList is a list of ServiceEndpoint resources. // +kubebuilder:object:root=true type ServiceEndpointList struct { diff --git a/api/v1alpha1/serviceroute_types_deepcopy.go b/api/v1alpha1/serviceroute_types_deepcopy.go index 5b851f5..7a1e0f2 100644 --- a/api/v1alpha1/serviceroute_types_deepcopy.go +++ b/api/v1alpha1/serviceroute_types_deepcopy.go @@ -10,6 +10,11 @@ import ( func (in *ServiceEndpoint) DeepCopyInto(out *ServiceEndpoint) { *out = *in in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Spec.AttachmentRef != nil { + in, out := &in.Spec.AttachmentRef, &out.Spec.AttachmentRef + *out = new(ServiceEndpointAttachmentReference) + **out = **in + } } func (in *ServiceEndpoint) DeepCopy() *ServiceEndpoint { @@ -139,3 +144,16 @@ func (in *ServiceEndpointReference) DeepCopy() *ServiceEndpointReference { in.DeepCopyInto(out) return out } + +func (in *ServiceEndpointAttachmentReference) DeepCopyInto(out *ServiceEndpointAttachmentReference) { + *out = *in +} + +func (in *ServiceEndpointAttachmentReference) DeepCopy() *ServiceEndpointAttachmentReference { + if in == nil { + return nil + } + out := new(ServiceEndpointAttachmentReference) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/network.datumapis.com_serviceendpoints.yaml b/config/crd/network.datumapis.com_serviceendpoints.yaml index 2fb9970..fa04874 100644 --- a/config/crd/network.datumapis.com_serviceendpoints.yaml +++ b/config/crd/network.datumapis.com_serviceendpoints.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.21.0 name: serviceendpoints.network.datumapis.com spec: group: network.datumapis.com @@ -62,6 +62,26 @@ spec: x-kubernetes-validations: - message: address must be an IP address rule: isIP(self) + attachmentRef: + description: |- + AttachmentRef identifies the Cloud API VPCAttachment that hosts this + endpoint when the service is delivered through a private VPC path. + The network API keeps this reference opaque; the consuming controller + resolves it against the Cloud API. + properties: + name: + description: Name is the Cloud API VPCAttachment name. + minLength: 1 + type: string + namespace: + description: Namespace is the namespace containing the Cloud API + VPCAttachment. + minLength: 1 + type: string + required: + - name + - namespace + type: object port: description: Port is the service port. format: int32 From bf0955ada02879a8762ad81d0ca7cdb9dd951ad7 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 30 Sep 2026 16:11:17 -0500 Subject: [PATCH 3/7] feat(api): reference service endpoint attachment --- config/crd/network.datumapis.com_serviceroutepolicies.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/crd/network.datumapis.com_serviceroutepolicies.yaml b/config/crd/network.datumapis.com_serviceroutepolicies.yaml index 67d33c3..558b776 100644 --- a/config/crd/network.datumapis.com_serviceroutepolicies.yaml +++ b/config/crd/network.datumapis.com_serviceroutepolicies.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.21.0 name: serviceroutepolicies.network.datumapis.com spec: group: network.datumapis.com From ce5fa0d7dd95307cb2aff3087ce424ab1516a360 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 6 Oct 2026 13:10:27 -0500 Subject: [PATCH 4/7] fix(api): make private service placement explicit --- api/v1alpha1/serviceroute_types.go | 17 ++ api/v1alpha1/serviceroute_types_deepcopy.go | 159 ------------ api/v1alpha1/zz_generated.deepcopy.go | 226 ++++++++++++++++++ ...etwork.datumapis.com_serviceendpoints.yaml | 12 +- ...rk.datumapis.com_serviceroutepolicies.yaml | 2 +- docs/api/bgp.md | 173 ++++++++++++++ docs/api/gateway.md | 177 ++++++++++++++ 7 files changed, 605 insertions(+), 161 deletions(-) delete mode 100644 api/v1alpha1/serviceroute_types_deepcopy.go diff --git a/api/v1alpha1/serviceroute_types.go b/api/v1alpha1/serviceroute_types.go index c931051..ee508d5 100644 --- a/api/v1alpha1/serviceroute_types.go +++ b/api/v1alpha1/serviceroute_types.go @@ -41,6 +41,13 @@ type ServiceEndpointSpec struct { // +kubebuilder:validation:Required Protocol NetworkRuleProtocol `json:"protocol"` + // DeliveryMode defines where the service backing attachment must run. The + // current dataplane supports only a backing attachment on every node that + // has selected consumers; it does not forward private-service traffic to a + // remote node. + // +kubebuilder:validation:Required + DeliveryMode ServiceEndpointDeliveryMode `json:"deliveryMode"` + // AttachmentRef identifies the Cloud API VPCAttachment that hosts this // endpoint when the service is delivered through a private VPC path. // The network API keeps this reference opaque; the consuming controller @@ -54,6 +61,16 @@ type ServiceEndpointSpec struct { Region string `json:"region,omitempty"` } +// ServiceEndpointDeliveryMode defines the placement contract for a service. +// +kubebuilder:validation:Enum=NodeLocal +type ServiceEndpointDeliveryMode string + +const ( + // ServiceEndpointDeliveryModeNodeLocal requires the endpoint's backing + // attachment to be present on each node where selected consumers run. + ServiceEndpointDeliveryModeNodeLocal ServiceEndpointDeliveryMode = "NodeLocal" +) + // ServiceEndpointAttachmentReference identifies the private attachment that // backs a platform service endpoint. type ServiceEndpointAttachmentReference struct { diff --git a/api/v1alpha1/serviceroute_types_deepcopy.go b/api/v1alpha1/serviceroute_types_deepcopy.go deleted file mode 100644 index 7a1e0f2..0000000 --- a/api/v1alpha1/serviceroute_types_deepcopy.go +++ /dev/null @@ -1,159 +0,0 @@ -// Code generated by controller-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" -) - -func (in *ServiceEndpoint) DeepCopyInto(out *ServiceEndpoint) { - *out = *in - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - if in.Spec.AttachmentRef != nil { - in, out := &in.Spec.AttachmentRef, &out.Spec.AttachmentRef - *out = new(ServiceEndpointAttachmentReference) - **out = **in - } -} - -func (in *ServiceEndpoint) DeepCopy() *ServiceEndpoint { - if in == nil { - return nil - } - out := new(ServiceEndpoint) - in.DeepCopyInto(out) - return out -} - -func (in *ServiceEndpoint) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -func (in *ServiceEndpointList) DeepCopyInto(out *ServiceEndpointList) { - *out = *in - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]ServiceEndpoint, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -func (in *ServiceEndpointList) DeepCopy() *ServiceEndpointList { - if in == nil { - return nil - } - out := new(ServiceEndpointList) - in.DeepCopyInto(out) - return out -} - -func (in *ServiceEndpointList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -func (in *ServiceRoutePolicy) DeepCopyInto(out *ServiceRoutePolicy) { - *out = *in - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -func (in *ServiceRoutePolicy) DeepCopy() *ServiceRoutePolicy { - if in == nil { - return nil - } - out := new(ServiceRoutePolicy) - in.DeepCopyInto(out) - return out -} - -func (in *ServiceRoutePolicy) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -func (in *ServiceRoutePolicyList) DeepCopyInto(out *ServiceRoutePolicyList) { - *out = *in - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]ServiceRoutePolicy, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -func (in *ServiceRoutePolicyList) DeepCopy() *ServiceRoutePolicyList { - if in == nil { - return nil - } - out := new(ServiceRoutePolicyList) - in.DeepCopyInto(out) - return out -} - -func (in *ServiceRoutePolicyList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -func (in *ServiceRoutePolicySpec) DeepCopyInto(out *ServiceRoutePolicySpec) { - *out = *in - in.ServiceRef.DeepCopyInto(&out.ServiceRef) - in.AttachmentSelector.DeepCopyInto(&out.AttachmentSelector) - if in.ProtocolPorts != nil { - in, out := &in.ProtocolPorts, &out.ProtocolPorts - *out = make([]ServiceRouteProtocolPort, len(*in)) - copy(*out, *in) - } -} - -func (in *ServiceRoutePolicyStatus) DeepCopyInto(out *ServiceRoutePolicyStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) - copy(*out, *in) - } -} - -func (in *ServiceEndpointReference) DeepCopyInto(out *ServiceEndpointReference) { - *out = *in -} - -func (in *ServiceEndpointReference) DeepCopy() *ServiceEndpointReference { - if in == nil { - return nil - } - out := new(ServiceEndpointReference) - in.DeepCopyInto(out) - return out -} - -func (in *ServiceEndpointAttachmentReference) DeepCopyInto(out *ServiceEndpointAttachmentReference) { - *out = *in -} - -func (in *ServiceEndpointAttachmentReference) DeepCopy() *ServiceEndpointAttachmentReference { - if in == nil { - return nil - } - out := new(ServiceEndpointAttachmentReference) - in.DeepCopyInto(out) - return out -} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 17eafb4..1985cbc 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1913,6 +1913,232 @@ func (in *RouterTarget) DeepCopy() *RouterTarget { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceEndpoint) DeepCopyInto(out *ServiceEndpoint) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceEndpoint. +func (in *ServiceEndpoint) DeepCopy() *ServiceEndpoint { + if in == nil { + return nil + } + out := new(ServiceEndpoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceEndpoint) 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 *ServiceEndpointAttachmentReference) DeepCopyInto(out *ServiceEndpointAttachmentReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceEndpointAttachmentReference. +func (in *ServiceEndpointAttachmentReference) DeepCopy() *ServiceEndpointAttachmentReference { + if in == nil { + return nil + } + out := new(ServiceEndpointAttachmentReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceEndpointList) DeepCopyInto(out *ServiceEndpointList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServiceEndpoint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceEndpointList. +func (in *ServiceEndpointList) DeepCopy() *ServiceEndpointList { + if in == nil { + return nil + } + out := new(ServiceEndpointList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceEndpointList) 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 *ServiceEndpointReference) DeepCopyInto(out *ServiceEndpointReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceEndpointReference. +func (in *ServiceEndpointReference) DeepCopy() *ServiceEndpointReference { + if in == nil { + return nil + } + out := new(ServiceEndpointReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceEndpointSpec) DeepCopyInto(out *ServiceEndpointSpec) { + *out = *in + if in.AttachmentRef != nil { + in, out := &in.AttachmentRef, &out.AttachmentRef + *out = new(ServiceEndpointAttachmentReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceEndpointSpec. +func (in *ServiceEndpointSpec) DeepCopy() *ServiceEndpointSpec { + if in == nil { + return nil + } + out := new(ServiceEndpointSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceRoutePolicy) DeepCopyInto(out *ServiceRoutePolicy) { + *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 ServiceRoutePolicy. +func (in *ServiceRoutePolicy) DeepCopy() *ServiceRoutePolicy { + if in == nil { + return nil + } + out := new(ServiceRoutePolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceRoutePolicy) 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 *ServiceRoutePolicyList) DeepCopyInto(out *ServiceRoutePolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServiceRoutePolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceRoutePolicyList. +func (in *ServiceRoutePolicyList) DeepCopy() *ServiceRoutePolicyList { + if in == nil { + return nil + } + out := new(ServiceRoutePolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServiceRoutePolicyList) 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 *ServiceRoutePolicySpec) DeepCopyInto(out *ServiceRoutePolicySpec) { + *out = *in + out.ServiceRef = in.ServiceRef + in.AttachmentSelector.DeepCopyInto(&out.AttachmentSelector) + if in.ProtocolPorts != nil { + in, out := &in.ProtocolPorts, &out.ProtocolPorts + *out = make([]ServiceRouteProtocolPort, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceRoutePolicySpec. +func (in *ServiceRoutePolicySpec) DeepCopy() *ServiceRoutePolicySpec { + if in == nil { + return nil + } + out := new(ServiceRoutePolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceRoutePolicyStatus) DeepCopyInto(out *ServiceRoutePolicyStatus) { + *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 ServiceRoutePolicyStatus. +func (in *ServiceRoutePolicyStatus) DeepCopy() *ServiceRoutePolicyStatus { + if in == nil { + return nil + } + out := new(ServiceRoutePolicyStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceRouteProtocolPort) DeepCopyInto(out *ServiceRouteProtocolPort) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceRouteProtocolPort. +func (in *ServiceRouteProtocolPort) DeepCopy() *ServiceRouteProtocolPort { + if in == nil { + return nil + } + out := new(ServiceRouteProtocolPort) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceVIPBinding) DeepCopyInto(out *ServiceVIPBinding) { *out = *in diff --git a/config/crd/network.datumapis.com_serviceendpoints.yaml b/config/crd/network.datumapis.com_serviceendpoints.yaml index fa04874..96d1db4 100644 --- a/config/crd/network.datumapis.com_serviceendpoints.yaml +++ b/config/crd/network.datumapis.com_serviceendpoints.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.21.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: serviceendpoints.network.datumapis.com spec: group: network.datumapis.com @@ -82,6 +82,15 @@ spec: - name - namespace type: object + deliveryMode: + description: |- + DeliveryMode defines where the service backing attachment must run. The + current dataplane supports only a backing attachment on every node that + has selected consumers; it does not forward private-service traffic to a + remote node. + enum: + - NodeLocal + type: string port: description: Port is the service port. format: int32 @@ -107,6 +116,7 @@ spec: type: string required: - address + - deliveryMode - port - protocol - serviceClass diff --git a/config/crd/network.datumapis.com_serviceroutepolicies.yaml b/config/crd/network.datumapis.com_serviceroutepolicies.yaml index 558b776..67d33c3 100644 --- a/config/crd/network.datumapis.com_serviceroutepolicies.yaml +++ b/config/crd/network.datumapis.com_serviceroutepolicies.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.21.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: serviceroutepolicies.network.datumapis.com spec: group: network.datumapis.com diff --git a/docs/api/bgp.md b/docs/api/bgp.md index d8345bb..d04bc04 100644 --- a/docs/api/bgp.md +++ b/docs/api/bgp.md @@ -16,6 +16,8 @@ Package v1alpha1 contains API Schema definitions for the network.datumapis.com/v - [BGPRouter](#bgprouter) - [BGPVRFInstance](#bgpvrfinstance) - [EgressShard](#egressshard) +- [ServiceEndpoint](#serviceendpoint) +- [ServiceRoutePolicy](#serviceroutepolicy) - [ServiceVIPBinding](#servicevipbinding) @@ -1407,6 +1409,177 @@ _Appears in:_ | `both` | SendCommunityTypeBoth sends both standard and extended communities.
| +#### ServiceEndpoint + + + +ServiceEndpoint publishes a platform service that can be reached through a +VPC attachment. The object describes the service contract; the controller +that owns the service is responsible for its health and implementation. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `network.datumapis.com/v1alpha1` | | | +| `kind` _string_ | `ServiceEndpoint` | | | +| `kind` _string_ | Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds | | | +| `apiVersion` _string_ | APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[ServiceEndpointSpec](#serviceendpointspec)_ | | | | + + +#### ServiceEndpointAttachmentReference + + + +ServiceEndpointAttachmentReference identifies the private attachment that +backs a platform service endpoint. + + + +_Appears in:_ +- [ServiceEndpointSpec](#serviceendpointspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `namespace` _string_ | Namespace is the namespace containing the Cloud API VPCAttachment. | | MinLength: 1
| +| `name` _string_ | Name is the Cloud API VPCAttachment name. | | MinLength: 1
| + + +#### ServiceEndpointDeliveryMode + +_Underlying type:_ _string_ + +ServiceEndpointDeliveryMode defines the placement contract for a service. + +_Validation:_ +- Enum: [NodeLocal] + +_Appears in:_ +- [ServiceEndpointSpec](#serviceendpointspec) + +| Field | Description | +| --- | --- | +| `NodeLocal` | ServiceEndpointDeliveryModeNodeLocal requires the endpoint's backing
attachment to be present on each node where selected consumers run.
| + + +#### ServiceEndpointReference + + + +ServiceEndpointReference identifies a ServiceEndpoint in the policy's +namespace. + + + +_Appears in:_ +- [ServiceRoutePolicySpec](#serviceroutepolicyspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | Name is the ServiceEndpoint name. | | MinLength: 1
| + + +#### ServiceEndpointSpec + + + +ServiceEndpointSpec defines a platform service address and its scope. + + + +_Appears in:_ +- [ServiceEndpoint](#serviceendpoint) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `serviceClass` _string_ | ServiceClass identifies the platform capability implemented by this
endpoint, for example "ipv6-egress-dns". | | MinLength: 1
Required: \{\}
| +| `address` _string_ | Address is the stable address that consumers use to reach the service. | | Required: \{\}
| +| `port` _integer_ | Port is the service port. | | Maximum: 65535
Minimum: 1
Required: \{\}
| +| `deliveryMode` _[ServiceEndpointDeliveryMode](#serviceendpointdeliverymode)_ | DeliveryMode defines where the service backing attachment must run. The
current dataplane supports only a backing attachment on every node that
has selected consumers; it does not forward private-service traffic to a
remote node. | | Enum: [NodeLocal]
Required: \{\}
| +| `attachmentRef` _[ServiceEndpointAttachmentReference](#serviceendpointattachmentreference)_ | AttachmentRef identifies the Cloud API VPCAttachment that hosts this
endpoint when the service is delivered through a private VPC path.
The network API keeps this reference opaque; the consuming controller
resolves it against the Cloud API. | | | +| `region` _string_ | Region limits endpoint selection to a region. An empty value means the
endpoint is not region-scoped. | | | + + +#### ServiceRoutePolicy + + + +ServiceRoutePolicy selects VPC attachments that should receive a route to a +platform ServiceEndpoint. The selector is evaluated by the network control +plane; Galactic compiles the selected attachments into dataplane state. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `network.datumapis.com/v1alpha1` | | | +| `kind` _string_ | `ServiceRoutePolicy` | | | +| `kind` _string_ | Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds | | | +| `apiVersion` _string_ | APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[ServiceRoutePolicySpec](#serviceroutepolicyspec)_ | | | | +| `status` _[ServiceRoutePolicyStatus](#serviceroutepolicystatus)_ | | | | + + +#### ServiceRoutePolicySpec + + + +ServiceRoutePolicySpec defines service selection and attachment eligibility. + + + +_Appears in:_ +- [ServiceRoutePolicy](#serviceroutepolicy) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `serviceRef` _[ServiceEndpointReference](#serviceendpointreference)_ | ServiceRef identifies the ServiceEndpoint in this namespace. | | Required: \{\}
| +| `attachmentSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#labelselector-v1-meta)_ | AttachmentSelector selects eligible VPC attachments by authoritative
labels. Labels that grant access to platform services must be assigned by
the network control plane, not by tenants. | | Required: \{\}
| +| `protocolPorts` _[ServiceRouteProtocolPort](#servicerouteprotocolport) array_ | ProtocolPorts limits the traffic that the route is intended to carry.
An empty list means the endpoint's declared port and protocol are used. | | | +| `region` _string_ | Region limits selected attachments to a region. An empty value means the
policy applies in every region where the endpoint is available. | | | + + +#### ServiceRoutePolicyStatus + + + +ServiceRoutePolicyStatus contains durable policy conditions only. Per- +attachment selection and programming state belongs in metrics and logs. + + + +_Appears in:_ +- [ServiceRoutePolicy](#serviceroutepolicy) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `observedGeneration` _integer_ | ObservedGeneration is the .metadata.generation this status was computed from. | | | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#condition-v1-meta) array_ | Conditions contains durable acceptance and service-availability state. | | | + + +#### ServiceRouteProtocolPort + + + +ServiceRouteProtocolPort identifies an allowed transport protocol and port. + + + +_Appears in:_ +- [ServiceRoutePolicySpec](#serviceroutepolicyspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `port` _integer_ | Port is the transport port. | | Maximum: 65535
Minimum: 1
Required: \{\}
| + + #### ServiceVIPBinding diff --git a/docs/api/gateway.md b/docs/api/gateway.md index 7cda32a..77e213e 100644 --- a/docs/api/gateway.md +++ b/docs/api/gateway.md @@ -13,6 +13,8 @@ Package v1alpha1 contains API Schema definitions for the network.datumapis.com/v - [NetworkEgressPolicy](#networkegresspolicy) - [NetworkGateway](#networkgateway) - [NetworkRule](#networkrule) +- [ServiceEndpoint](#serviceendpoint) +- [ServiceRoutePolicy](#serviceroutepolicy) - [ServiceVIPBinding](#servicevipbinding) @@ -417,6 +419,8 @@ _Validation:_ _Appears in:_ - [NetworkRuleSpec](#networkrulespec) +- [ServiceEndpointSpec](#serviceendpointspec) +- [ServiceRouteProtocolPort](#servicerouteprotocolport) - [ServiceVIPBindingSpec](#servicevipbindingspec) | Field | Description | @@ -464,6 +468,179 @@ _Appears in:_ | `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#condition-v1-meta) array_ | Conditions contains the standard conditions for this resource. | | | +#### ServiceEndpoint + + + +ServiceEndpoint publishes a platform service that can be reached through a +VPC attachment. The object describes the service contract; the controller +that owns the service is responsible for its health and implementation. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `network.datumapis.com/v1alpha1` | | | +| `kind` _string_ | `ServiceEndpoint` | | | +| `kind` _string_ | Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds | | | +| `apiVersion` _string_ | APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[ServiceEndpointSpec](#serviceendpointspec)_ | | | | + + +#### ServiceEndpointAttachmentReference + + + +ServiceEndpointAttachmentReference identifies the private attachment that +backs a platform service endpoint. + + + +_Appears in:_ +- [ServiceEndpointSpec](#serviceendpointspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `namespace` _string_ | Namespace is the namespace containing the Cloud API VPCAttachment. | | MinLength: 1
| +| `name` _string_ | Name is the Cloud API VPCAttachment name. | | MinLength: 1
| + + +#### ServiceEndpointDeliveryMode + +_Underlying type:_ _string_ + +ServiceEndpointDeliveryMode defines the placement contract for a service. + +_Validation:_ +- Enum: [NodeLocal] + +_Appears in:_ +- [ServiceEndpointSpec](#serviceendpointspec) + +| Field | Description | +| --- | --- | +| `NodeLocal` | ServiceEndpointDeliveryModeNodeLocal requires the endpoint's backing
attachment to be present on each node where selected consumers run.
| + + +#### ServiceEndpointReference + + + +ServiceEndpointReference identifies a ServiceEndpoint in the policy's +namespace. + + + +_Appears in:_ +- [ServiceRoutePolicySpec](#serviceroutepolicyspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | Name is the ServiceEndpoint name. | | MinLength: 1
| + + +#### ServiceEndpointSpec + + + +ServiceEndpointSpec defines a platform service address and its scope. + + + +_Appears in:_ +- [ServiceEndpoint](#serviceendpoint) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `serviceClass` _string_ | ServiceClass identifies the platform capability implemented by this
endpoint, for example "ipv6-egress-dns". | | MinLength: 1
Required: \{\}
| +| `address` _string_ | Address is the stable address that consumers use to reach the service. | | Required: \{\}
| +| `port` _integer_ | Port is the service port. | | Maximum: 65535
Minimum: 1
Required: \{\}
| +| `protocol` _[NetworkRuleProtocol](#networkruleprotocol)_ | Protocol is the transport protocol accepted by the endpoint. | | Enum: [tcp udp]
Required: \{\}
| +| `deliveryMode` _[ServiceEndpointDeliveryMode](#serviceendpointdeliverymode)_ | DeliveryMode defines where the service backing attachment must run. The
current dataplane supports only a backing attachment on every node that
has selected consumers; it does not forward private-service traffic to a
remote node. | | Enum: [NodeLocal]
Required: \{\}
| +| `attachmentRef` _[ServiceEndpointAttachmentReference](#serviceendpointattachmentreference)_ | AttachmentRef identifies the Cloud API VPCAttachment that hosts this
endpoint when the service is delivered through a private VPC path.
The network API keeps this reference opaque; the consuming controller
resolves it against the Cloud API. | | | +| `region` _string_ | Region limits endpoint selection to a region. An empty value means the
endpoint is not region-scoped. | | | + + +#### ServiceRoutePolicy + + + +ServiceRoutePolicy selects VPC attachments that should receive a route to a +platform ServiceEndpoint. The selector is evaluated by the network control +plane; Galactic compiles the selected attachments into dataplane state. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `network.datumapis.com/v1alpha1` | | | +| `kind` _string_ | `ServiceRoutePolicy` | | | +| `kind` _string_ | Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds | | | +| `apiVersion` _string_ | APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[ServiceRoutePolicySpec](#serviceroutepolicyspec)_ | | | | +| `status` _[ServiceRoutePolicyStatus](#serviceroutepolicystatus)_ | | | | + + +#### ServiceRoutePolicySpec + + + +ServiceRoutePolicySpec defines service selection and attachment eligibility. + + + +_Appears in:_ +- [ServiceRoutePolicy](#serviceroutepolicy) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `serviceRef` _[ServiceEndpointReference](#serviceendpointreference)_ | ServiceRef identifies the ServiceEndpoint in this namespace. | | Required: \{\}
| +| `attachmentSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#labelselector-v1-meta)_ | AttachmentSelector selects eligible VPC attachments by authoritative
labels. Labels that grant access to platform services must be assigned by
the network control plane, not by tenants. | | Required: \{\}
| +| `protocolPorts` _[ServiceRouteProtocolPort](#servicerouteprotocolport) array_ | ProtocolPorts limits the traffic that the route is intended to carry.
An empty list means the endpoint's declared port and protocol are used. | | | +| `region` _string_ | Region limits selected attachments to a region. An empty value means the
policy applies in every region where the endpoint is available. | | | + + +#### ServiceRoutePolicyStatus + + + +ServiceRoutePolicyStatus contains durable policy conditions only. Per- +attachment selection and programming state belongs in metrics and logs. + + + +_Appears in:_ +- [ServiceRoutePolicy](#serviceroutepolicy) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `observedGeneration` _integer_ | ObservedGeneration is the .metadata.generation this status was computed from. | | | +| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#condition-v1-meta) array_ | Conditions contains durable acceptance and service-availability state. | | | + + +#### ServiceRouteProtocolPort + + + +ServiceRouteProtocolPort identifies an allowed transport protocol and port. + + + +_Appears in:_ +- [ServiceRoutePolicySpec](#serviceroutepolicyspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `protocol` _[NetworkRuleProtocol](#networkruleprotocol)_ | Protocol is the transport protocol. | | Enum: [tcp udp]
Required: \{\}
| +| `port` _integer_ | Port is the transport port. | | Maximum: 65535
Minimum: 1
Required: \{\}
| + + #### ServiceVIPBinding From 248c62692a8f6dbed69b130cd65263442bdbda1f Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 6 Oct 2026 13:12:06 -0500 Subject: [PATCH 5/7] fix(api): define service route acceptance condition --- api/v1alpha1/serviceroute_types.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/v1alpha1/serviceroute_types.go b/api/v1alpha1/serviceroute_types.go index ee508d5..5374516 100644 --- a/api/v1alpha1/serviceroute_types.go +++ b/api/v1alpha1/serviceroute_types.go @@ -169,6 +169,12 @@ type ServiceRoutePolicyStatus struct { Conditions []metav1.Condition `json:"conditions,omitempty"` } +const ( + // ConditionTypeServiceRouteAccepted reports whether the policy is valid and + // can be compiled by the dataplane controllers. + ConditionTypeServiceRouteAccepted = "Accepted" +) + // ServiceRoutePolicyList is a list of ServiceRoutePolicy resources. // +kubebuilder:object:root=true type ServiceRoutePolicyList struct { From 0f099eb3b0eabfd158d9b40500b69823428a5d4a Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 6 Oct 2026 13:57:16 -0500 Subject: [PATCH 6/7] feat(api): prefer node-local service replicas --- api/v1alpha1/serviceroute_types.go | 22 +++++-- api/v1alpha1/serviceroute_types_test.go | 15 +++++ api/v1alpha1/zz_generated.deepcopy.go | 5 ++ ...etwork.datumapis.com_serviceendpoints.yaml | 62 +++++++++++++++++-- docs/api/bgp.md | 6 +- docs/api/gateway.md | 6 +- 6 files changed, 103 insertions(+), 13 deletions(-) diff --git a/api/v1alpha1/serviceroute_types.go b/api/v1alpha1/serviceroute_types.go index 5374516..380235c 100644 --- a/api/v1alpha1/serviceroute_types.go +++ b/api/v1alpha1/serviceroute_types.go @@ -19,6 +19,7 @@ type ServiceEndpoint struct { } // ServiceEndpointSpec defines a platform service address and its scope. +// +kubebuilder:validation:XValidation:rule="has(self.attachmentRef) != has(self.attachmentSelector)",message="exactly one of attachmentRef or attachmentSelector must be set" type ServiceEndpointSpec struct { // ServiceClass identifies the platform capability implemented by this // endpoint, for example "ipv6-egress-dns". @@ -41,10 +42,9 @@ type ServiceEndpointSpec struct { // +kubebuilder:validation:Required Protocol NetworkRuleProtocol `json:"protocol"` - // DeliveryMode defines where the service backing attachment must run. The - // current dataplane supports only a backing attachment on every node that - // has selected consumers; it does not forward private-service traffic to a - // remote node. + // DeliveryMode defines how a backing attachment is selected relative to a + // consumer. PreferNodeLocal uses a ready attachment on the consumer's node + // when one exists and otherwise falls back to a ready remote attachment. // +kubebuilder:validation:Required DeliveryMode ServiceEndpointDeliveryMode `json:"deliveryMode"` @@ -55,6 +55,13 @@ type ServiceEndpointSpec struct { // +optional AttachmentRef *ServiceEndpointAttachmentReference `json:"attachmentRef,omitempty"` + // AttachmentSelector selects the Cloud API VPCAttachments that host replicas + // of this endpoint. Labels used here must be assigned by the platform service + // controller, not by tenants. The dataplane selects one ready replica for + // each consumer according to DeliveryMode. + // +optional + AttachmentSelector *metav1.LabelSelector `json:"attachmentSelector,omitempty"` + // Region limits endpoint selection to a region. An empty value means the // endpoint is not region-scoped. // +optional @@ -62,13 +69,18 @@ type ServiceEndpointSpec struct { } // ServiceEndpointDeliveryMode defines the placement contract for a service. -// +kubebuilder:validation:Enum=NodeLocal +// +kubebuilder:validation:Enum=NodeLocal;PreferNodeLocal type ServiceEndpointDeliveryMode string const ( // ServiceEndpointDeliveryModeNodeLocal requires the endpoint's backing // attachment to be present on each node where selected consumers run. ServiceEndpointDeliveryModeNodeLocal ServiceEndpointDeliveryMode = "NodeLocal" + + // ServiceEndpointDeliveryModePreferNodeLocal selects a backing attachment on + // the consumer's node when possible and otherwise tunnels to a ready remote + // attachment while preserving the consumer source address. + ServiceEndpointDeliveryModePreferNodeLocal ServiceEndpointDeliveryMode = "PreferNodeLocal" ) // ServiceEndpointAttachmentReference identifies the private attachment that diff --git a/api/v1alpha1/serviceroute_types_test.go b/api/v1alpha1/serviceroute_types_test.go index 5af2aa8..7876bab 100644 --- a/api/v1alpha1/serviceroute_types_test.go +++ b/api/v1alpha1/serviceroute_types_test.go @@ -48,3 +48,18 @@ func TestServiceRoutePolicyListDeepCopy(t *testing.T) { t.Fatalf("DeepCopy shared selector values: got %q", got) } } + +func TestServiceEndpointDeepCopy(t *testing.T) { + original := &ServiceEndpoint{Spec: ServiceEndpointSpec{ + DeliveryMode: ServiceEndpointDeliveryModePreferNodeLocal, + AttachmentSelector: &metav1.LabelSelector{MatchLabels: map[string]string{ + "service.datumapis.com/name": "dns64", + }}, + }} + + copy := original.DeepCopy() + copy.Spec.AttachmentSelector.MatchLabels["service.datumapis.com/name"] = "other" + if got := original.Spec.AttachmentSelector.MatchLabels["service.datumapis.com/name"]; got != "dns64" { + t.Fatalf("DeepCopy shared endpoint selector labels: got %q", got) + } +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 1985cbc..8833b84 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2009,6 +2009,11 @@ func (in *ServiceEndpointSpec) DeepCopyInto(out *ServiceEndpointSpec) { *out = new(ServiceEndpointAttachmentReference) **out = **in } + if in.AttachmentSelector != nil { + in, out := &in.AttachmentSelector, &out.AttachmentSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceEndpointSpec. diff --git a/config/crd/network.datumapis.com_serviceendpoints.yaml b/config/crd/network.datumapis.com_serviceendpoints.yaml index 96d1db4..6a2cda9 100644 --- a/config/crd/network.datumapis.com_serviceendpoints.yaml +++ b/config/crd/network.datumapis.com_serviceendpoints.yaml @@ -82,14 +82,64 @@ spec: - name - namespace type: object + attachmentSelector: + description: |- + AttachmentSelector selects the Cloud API VPCAttachments that host replicas + of this endpoint. Labels used here must be assigned by the platform service + controller, not by tenants. The dataplane selects one ready replica for + each consumer according to DeliveryMode. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic deliveryMode: description: |- - DeliveryMode defines where the service backing attachment must run. The - current dataplane supports only a backing attachment on every node that - has selected consumers; it does not forward private-service traffic to a - remote node. + DeliveryMode defines how a backing attachment is selected relative to a + consumer. PreferNodeLocal uses a ready attachment on the consumer's node + when one exists and otherwise falls back to a ready remote attachment. enum: - NodeLocal + - PreferNodeLocal type: string port: description: Port is the service port. @@ -121,6 +171,10 @@ spec: - protocol - serviceClass type: object + x-kubernetes-validations: + - message: exactly one of attachmentRef or attachmentSelector must be + set + rule: has(self.attachmentRef) != has(self.attachmentSelector) type: object served: true storage: true diff --git a/docs/api/bgp.md b/docs/api/bgp.md index d04bc04..ff23d53 100644 --- a/docs/api/bgp.md +++ b/docs/api/bgp.md @@ -1456,7 +1456,7 @@ _Underlying type:_ _string_ ServiceEndpointDeliveryMode defines the placement contract for a service. _Validation:_ -- Enum: [NodeLocal] +- Enum: [NodeLocal PreferNodeLocal] _Appears in:_ - [ServiceEndpointSpec](#serviceendpointspec) @@ -1464,6 +1464,7 @@ _Appears in:_ | Field | Description | | --- | --- | | `NodeLocal` | ServiceEndpointDeliveryModeNodeLocal requires the endpoint's backing
attachment to be present on each node where selected consumers run.
| +| `PreferNodeLocal` | ServiceEndpointDeliveryModePreferNodeLocal selects a backing attachment on
the consumer's node when possible and otherwise tunnels to a ready remote
attachment while preserving the consumer source address.
| #### ServiceEndpointReference @@ -1499,8 +1500,9 @@ _Appears in:_ | `serviceClass` _string_ | ServiceClass identifies the platform capability implemented by this
endpoint, for example "ipv6-egress-dns". | | MinLength: 1
Required: \{\}
| | `address` _string_ | Address is the stable address that consumers use to reach the service. | | Required: \{\}
| | `port` _integer_ | Port is the service port. | | Maximum: 65535
Minimum: 1
Required: \{\}
| -| `deliveryMode` _[ServiceEndpointDeliveryMode](#serviceendpointdeliverymode)_ | DeliveryMode defines where the service backing attachment must run. The
current dataplane supports only a backing attachment on every node that
has selected consumers; it does not forward private-service traffic to a
remote node. | | Enum: [NodeLocal]
Required: \{\}
| +| `deliveryMode` _[ServiceEndpointDeliveryMode](#serviceendpointdeliverymode)_ | DeliveryMode defines how a backing attachment is selected relative to a
consumer. PreferNodeLocal uses a ready attachment on the consumer's node
when one exists and otherwise falls back to a ready remote attachment. | | Enum: [NodeLocal PreferNodeLocal]
Required: \{\}
| | `attachmentRef` _[ServiceEndpointAttachmentReference](#serviceendpointattachmentreference)_ | AttachmentRef identifies the Cloud API VPCAttachment that hosts this
endpoint when the service is delivered through a private VPC path.
The network API keeps this reference opaque; the consuming controller
resolves it against the Cloud API. | | | +| `attachmentSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#labelselector-v1-meta)_ | AttachmentSelector selects the Cloud API VPCAttachments that host replicas
of this endpoint. Labels used here must be assigned by the platform service
controller, not by tenants. The dataplane selects one ready replica for
each consumer according to DeliveryMode. | | | | `region` _string_ | Region limits endpoint selection to a region. An empty value means the
endpoint is not region-scoped. | | | diff --git a/docs/api/gateway.md b/docs/api/gateway.md index 77e213e..3e711cc 100644 --- a/docs/api/gateway.md +++ b/docs/api/gateway.md @@ -515,7 +515,7 @@ _Underlying type:_ _string_ ServiceEndpointDeliveryMode defines the placement contract for a service. _Validation:_ -- Enum: [NodeLocal] +- Enum: [NodeLocal PreferNodeLocal] _Appears in:_ - [ServiceEndpointSpec](#serviceendpointspec) @@ -523,6 +523,7 @@ _Appears in:_ | Field | Description | | --- | --- | | `NodeLocal` | ServiceEndpointDeliveryModeNodeLocal requires the endpoint's backing
attachment to be present on each node where selected consumers run.
| +| `PreferNodeLocal` | ServiceEndpointDeliveryModePreferNodeLocal selects a backing attachment on
the consumer's node when possible and otherwise tunnels to a ready remote
attachment while preserving the consumer source address.
| #### ServiceEndpointReference @@ -559,8 +560,9 @@ _Appears in:_ | `address` _string_ | Address is the stable address that consumers use to reach the service. | | Required: \{\}
| | `port` _integer_ | Port is the service port. | | Maximum: 65535
Minimum: 1
Required: \{\}
| | `protocol` _[NetworkRuleProtocol](#networkruleprotocol)_ | Protocol is the transport protocol accepted by the endpoint. | | Enum: [tcp udp]
Required: \{\}
| -| `deliveryMode` _[ServiceEndpointDeliveryMode](#serviceendpointdeliverymode)_ | DeliveryMode defines where the service backing attachment must run. The
current dataplane supports only a backing attachment on every node that
has selected consumers; it does not forward private-service traffic to a
remote node. | | Enum: [NodeLocal]
Required: \{\}
| +| `deliveryMode` _[ServiceEndpointDeliveryMode](#serviceendpointdeliverymode)_ | DeliveryMode defines how a backing attachment is selected relative to a
consumer. PreferNodeLocal uses a ready attachment on the consumer's node
when one exists and otherwise falls back to a ready remote attachment. | | Enum: [NodeLocal PreferNodeLocal]
Required: \{\}
| | `attachmentRef` _[ServiceEndpointAttachmentReference](#serviceendpointattachmentreference)_ | AttachmentRef identifies the Cloud API VPCAttachment that hosts this
endpoint when the service is delivered through a private VPC path.
The network API keeps this reference opaque; the consuming controller
resolves it against the Cloud API. | | | +| `attachmentSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#labelselector-v1-meta)_ | AttachmentSelector selects the Cloud API VPCAttachments that host replicas
of this endpoint. Labels used here must be assigned by the platform service
controller, not by tenants. The dataplane selects one ready replica for
each consumer according to DeliveryMode. | | | | `region` _string_ | Region limits endpoint selection to a region. An empty value means the
endpoint is not region-scoped. | | | From f996e70945d303e67ba533c0adca20d30e06e654 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 6 Oct 2026 15:19:59 -0500 Subject: [PATCH 7/7] docs(api): define overlapping service flow behavior --- api/v1alpha1/serviceroute_types.go | 4 ++++ config/crd/network.datumapis.com_serviceendpoints.yaml | 4 ++++ docs/api/bgp.md | 2 +- docs/api/gateway.md | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/serviceroute_types.go b/api/v1alpha1/serviceroute_types.go index 380235c..f5b7156 100644 --- a/api/v1alpha1/serviceroute_types.go +++ b/api/v1alpha1/serviceroute_types.go @@ -45,6 +45,10 @@ type ServiceEndpointSpec struct { // DeliveryMode defines how a backing attachment is selected relative to a // consumer. PreferNodeLocal uses a ready attachment on the consumer's node // when one exists and otherwise falls back to a ready remote attachment. + // The dataplane preserves the consumer source address. If consumers with + // overlapping address space present an identical TCP or UDP five-tuple to the + // same replica, it rejects the later ambiguous flow fail-closed rather than + // translate the source or risk returning traffic to the wrong VPC. // +kubebuilder:validation:Required DeliveryMode ServiceEndpointDeliveryMode `json:"deliveryMode"` diff --git a/config/crd/network.datumapis.com_serviceendpoints.yaml b/config/crd/network.datumapis.com_serviceendpoints.yaml index 6a2cda9..65e02ae 100644 --- a/config/crd/network.datumapis.com_serviceendpoints.yaml +++ b/config/crd/network.datumapis.com_serviceendpoints.yaml @@ -137,6 +137,10 @@ spec: DeliveryMode defines how a backing attachment is selected relative to a consumer. PreferNodeLocal uses a ready attachment on the consumer's node when one exists and otherwise falls back to a ready remote attachment. + The dataplane preserves the consumer source address. If consumers with + overlapping address space present an identical TCP or UDP five-tuple to the + same replica, it rejects the later ambiguous flow fail-closed rather than + translate the source or risk returning traffic to the wrong VPC. enum: - NodeLocal - PreferNodeLocal diff --git a/docs/api/bgp.md b/docs/api/bgp.md index ff23d53..cc6c93f 100644 --- a/docs/api/bgp.md +++ b/docs/api/bgp.md @@ -1500,7 +1500,7 @@ _Appears in:_ | `serviceClass` _string_ | ServiceClass identifies the platform capability implemented by this
endpoint, for example "ipv6-egress-dns". | | MinLength: 1
Required: \{\}
| | `address` _string_ | Address is the stable address that consumers use to reach the service. | | Required: \{\}
| | `port` _integer_ | Port is the service port. | | Maximum: 65535
Minimum: 1
Required: \{\}
| -| `deliveryMode` _[ServiceEndpointDeliveryMode](#serviceendpointdeliverymode)_ | DeliveryMode defines how a backing attachment is selected relative to a
consumer. PreferNodeLocal uses a ready attachment on the consumer's node
when one exists and otherwise falls back to a ready remote attachment. | | Enum: [NodeLocal PreferNodeLocal]
Required: \{\}
| +| `deliveryMode` _[ServiceEndpointDeliveryMode](#serviceendpointdeliverymode)_ | DeliveryMode defines how a backing attachment is selected relative to a
consumer. PreferNodeLocal uses a ready attachment on the consumer's node
when one exists and otherwise falls back to a ready remote attachment.
The dataplane preserves the consumer source address. If consumers with
overlapping address space present an identical TCP or UDP five-tuple to the
same replica, it rejects the later ambiguous flow fail-closed rather than
translate the source or risk returning traffic to the wrong VPC. | | Enum: [NodeLocal PreferNodeLocal]
Required: \{\}
| | `attachmentRef` _[ServiceEndpointAttachmentReference](#serviceendpointattachmentreference)_ | AttachmentRef identifies the Cloud API VPCAttachment that hosts this
endpoint when the service is delivered through a private VPC path.
The network API keeps this reference opaque; the consuming controller
resolves it against the Cloud API. | | | | `attachmentSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#labelselector-v1-meta)_ | AttachmentSelector selects the Cloud API VPCAttachments that host replicas
of this endpoint. Labels used here must be assigned by the platform service
controller, not by tenants. The dataplane selects one ready replica for
each consumer according to DeliveryMode. | | | | `region` _string_ | Region limits endpoint selection to a region. An empty value means the
endpoint is not region-scoped. | | | diff --git a/docs/api/gateway.md b/docs/api/gateway.md index 3e711cc..179641d 100644 --- a/docs/api/gateway.md +++ b/docs/api/gateway.md @@ -560,7 +560,7 @@ _Appears in:_ | `address` _string_ | Address is the stable address that consumers use to reach the service. | | Required: \{\}
| | `port` _integer_ | Port is the service port. | | Maximum: 65535
Minimum: 1
Required: \{\}
| | `protocol` _[NetworkRuleProtocol](#networkruleprotocol)_ | Protocol is the transport protocol accepted by the endpoint. | | Enum: [tcp udp]
Required: \{\}
| -| `deliveryMode` _[ServiceEndpointDeliveryMode](#serviceendpointdeliverymode)_ | DeliveryMode defines how a backing attachment is selected relative to a
consumer. PreferNodeLocal uses a ready attachment on the consumer's node
when one exists and otherwise falls back to a ready remote attachment. | | Enum: [NodeLocal PreferNodeLocal]
Required: \{\}
| +| `deliveryMode` _[ServiceEndpointDeliveryMode](#serviceendpointdeliverymode)_ | DeliveryMode defines how a backing attachment is selected relative to a
consumer. PreferNodeLocal uses a ready attachment on the consumer's node
when one exists and otherwise falls back to a ready remote attachment.
The dataplane preserves the consumer source address. If consumers with
overlapping address space present an identical TCP or UDP five-tuple to the
same replica, it rejects the later ambiguous flow fail-closed rather than
translate the source or risk returning traffic to the wrong VPC. | | Enum: [NodeLocal PreferNodeLocal]
Required: \{\}
| | `attachmentRef` _[ServiceEndpointAttachmentReference](#serviceendpointattachmentreference)_ | AttachmentRef identifies the Cloud API VPCAttachment that hosts this
endpoint when the service is delivered through a private VPC path.
The network API keeps this reference opaque; the consuming controller
resolves it against the Cloud API. | | | | `attachmentSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v/#labelselector-v1-meta)_ | AttachmentSelector selects the Cloud API VPCAttachments that host replicas
of this endpoint. Labels used here must be assigned by the platform service
controller, not by tenants. The dataplane selects one ready replica for
each consumer according to DeliveryMode. | | | | `region` _string_ | Region limits endpoint selection to a region. An empty value means the
endpoint is not region-scoped. | | |