Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
81939f7
feat: add NetworkService API type
scotwells Aug 26, 2026
0d90e9f
feat: resolve NetworkService membership from claims
scotwells Aug 26, 2026
933534d
feat: add networkService HTTPProxy backend
scotwells Aug 26, 2026
6512c61
Merge branch 'proto/ns-httpproxy' into proto/network-service
scotwells Aug 26, 2026
ac6e9e7
test: validate the NetworkService prototype at the edge
scotwells Aug 26, 2026
7340a77
feat: stamp the serving location onto claims
scotwells Aug 26, 2026
22cd073
Merge remote-tracking branch 'origin/main' into proto/network-service
scotwells Aug 26, 2026
3c09206
feat: protect NetworkService with Milo IAM
scotwells Aug 26, 2026
8e1bd29
feat: select interfaces, not claims, for membership
scotwells Aug 26, 2026
5bd77ac
feat: base service member health on the holder
scotwells Aug 26, 2026
3e7da88
fix: drop a departed holder's word on unbind
scotwells Aug 26, 2026
4d2b3e3
docs: record what a HolderAvailable reset depends on
scotwells Aug 26, 2026
e24f8e3
fix: reject backend TLS on networkService backends
scotwells Aug 26, 2026
824bea6
test: pin why a member-less service keeps its slice
scotwells Aug 26, 2026
3376f1f
fix: stop the e2e manager racing the karmada hub secret
scotwells Aug 26, 2026
57ae46f
feat: drop the EndpointsReachable condition
scotwells Aug 26, 2026
9abaf6d
feat: publish vpc endpointslices to the federation hub
scotwells Aug 26, 2026
17a860e
feat: mirror networks onto the federation hub
scotwells Aug 27, 2026
10a09bf
fix: bind networkService backends to the tenant VRF
scotwells Aug 27, 2026
d824a34
fix: reach a member only inside its network
scotwells Aug 27, 2026
54b8e8b
feat: brand the offline page for empty services
scotwells Aug 27, 2026
2d4153f
feat: run the real VRF sidecar, not a placeholder
scotwells Aug 27, 2026
f1d16ec
fix: give the VRF sidecar an API server token
scotwells Aug 27, 2026
e28af3c
fix: make the VRF sidecar's NET_ADMIN effective
scotwells Aug 27, 2026
c20279a
Merge pull request #414 from datum-cloud/fix/vrf-sidecar-capability
scotwells Aug 27, 2026
532b967
feat: carry a VPC pod to the edge only when served
scotwells Aug 27, 2026
bd4b8ee
Merge pull request #415 from datum-cloud/feat/411-filter-federated-sl…
scotwells Aug 27, 2026
9233c18
Merge remote-tracking branch 'origin/main' into proto/network-service
scotwells Aug 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,12 @@ resources:
kind: NetworkInterfaceClaim
path: go.datum.net/network-services-operator/api/v1alpha
version: v1alpha
- api:
crdVersion: v1
namespaced: true
domain: datumapis.com
group: networking
kind: NetworkService
path: go.datum.net/network-services-operator/api/v1alpha
version: v1alpha
version: "3"
6 changes: 5 additions & 1 deletion Taskfile.test-infra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ tasks:
- task: billing-usage-collector
- task: external-dns
- task: karmada-up
# Same shape as ipam-kubeconfig above: the manager and the cell both mount
# this and neither can federate without it, so both wait in
# ContainerCreating until it lands here.
- task: karmada-hub-secret
- task: location-fixtures
- task: link-clusters
Expand Down Expand Up @@ -411,6 +414,7 @@ tasks:
desc: "Install the NSO CRDs the replicator mirrors into the downstream cluster (the Gateway-API/EG CRDs come from eg-crds)."
cmds:
- kubectl --context {{.DOWNSTREAM_CTX}} apply -f config/crd/bases/networking.datumapis.com_connectors.yaml
- kubectl --context {{.DOWNSTREAM_CTX}} apply -f config/crd/bases/networking.datumapis.com_edgereachabilities.yaml
- kubectl --context {{.DOWNSTREAM_CTX}} apply -f config/crd/bases/networking.datumapis.com_httpproxies.yaml
- kubectl --context {{.DOWNSTREAM_CTX}} apply -f config/crd/bases/networking.datumapis.com_trafficprotectionpolicies.yaml
- kubectl --context {{.DOWNSTREAM_CTX}} apply -f config/crd/bases/networking.datumapis.com_servinglocations.yaml
Expand Down Expand Up @@ -1090,7 +1094,7 @@ tasks:
vars:
# Scenarios authored against this env's cluster names and downstream path.
# Older fixtures targeting the previous cluster names are excluded here.
DEFAULT_SCENARIOS: extension-server-smoke waf-enforcement branded-error-page connector-offline-503 atomic-reject-isolation
DEFAULT_SCENARIOS: extension-server-smoke waf-enforcement branded-error-page connector-offline-503 atomic-reject-isolation networkservice-endpoints
# CLI_ARGS (after --) wins; else SCENARIOS env; else the default set.
SELECTED: '{{.CLI_ARGS | default .SCENARIOS | default .DEFAULT_SCENARIOS}}'
deps:
Expand Down
57 changes: 57 additions & 0 deletions api/v1alpha/edgereachability_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: AGPL-3.0-only

package v1alpha

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EdgeReachabilityName is the name of the single record a namespace holds.
// One record answers for the whole namespace, so a reader gets its answer with
// a get rather than a list it has to decide is complete.
const EdgeReachabilityName = "default"

// EdgeReachabilitySpec is the set of workload addresses in one project
// namespace that an edge is expected to reach.
type EdgeReachabilitySpec struct {
// addresses are the workload addresses currently behind a proxy, one entry
// per address, with no prefix length. An empty list is a real answer: it
// says the project publishes nothing, which is different from no record at
// all.
//
// +kubebuilder:validation:Optional
// +kubebuilder:validation:MaxItems=8192
Addresses []string `json:"addresses,omitempty"`
}

// +kubebuilder:object:root=true

// EdgeReachability records which of a project's workload addresses are behind
// an HTTPProxy, so the platform carries a workload's location to the edges that
// serve it and stops carrying it everywhere else.
//
// It is written by the control plane onto the federation hub and read by the
// cells publishing into it. No consumer creates or edits one, and nothing in a
// project control plane holds one.
//
// Absence of the record means the control plane has not answered for this
// namespace yet, and a reader must keep publishing rather than treat silence as
// a withdrawal. An empty list is the answer that withdraws.
// +kubebuilder:printcolumn:name="Addresses",type=integer,JSONPath=".spec.addresses.length()"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
type EdgeReachability struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

// +kubebuilder:validation:Optional
Spec EdgeReachabilitySpec `json:"spec,omitempty"`
}

// +kubebuilder:object:root=true

// EdgeReachabilityList contains a list of EdgeReachability.
type EdgeReachabilityList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []EdgeReachability `json:"items"`
}
4 changes: 4 additions & 0 deletions api/v1alpha/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(GroupVersion,
&Domain{},
&DomainList{},
&EdgeReachability{},
&EdgeReachabilityList{},
&HTTPProxy{},
&HTTPProxyList{},
&Location{},
Expand All @@ -44,6 +46,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&NetworkInterfaceClaimList{},
&NetworkPolicy{},
&NetworkPolicyList{},
&NetworkService{},
&NetworkServiceList{},
&ServingLocation{},
&ServingLocationList{},
&Subnet{},
Expand Down
49 changes: 48 additions & 1 deletion api/v1alpha/httpproxy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ type HTTPProxyRule struct {
Backends []HTTPProxyRuleBackend `json:"backends,omitempty"`
}

// +kubebuilder:validation:XValidation:message="endpoint is required unless instance is set, and instance is mutually exclusive with endpoint and connector",rule="has(self.instance) ? (!has(self.endpoint) && !has(self.connector)) : has(self.endpoint)"
// +kubebuilder:validation:XValidation:message="endpoint is required unless instance or networkService is set; instance and networkService are mutually exclusive with each other and with endpoint and connector",rule="has(self.instance) ? (!has(self.endpoint) && !has(self.connector) && !has(self.networkService)) : (has(self.networkService) ? (!has(self.endpoint) && !has(self.connector)) : has(self.endpoint))"
// +kubebuilder:validation:XValidation:message="backend TLS is not supported for networkService backends",rule="has(self.networkService) ? !has(self.tls) : true"
type HTTPProxyRuleBackend struct {
// Endpoint for the backend. Must be a valid URL.
//
Expand Down Expand Up @@ -147,11 +148,24 @@ type HTTPProxyRuleBackend struct {
// +kubebuilder:validation:Optional
Instance *InstanceBackendRef `json:"instance,omitempty"`

// NetworkService references a NetworkService in the same namespace, and one
// of the ports it declares. Every member the service resolves to becomes an
// endpoint of this backend, so instances appearing, disappearing, and moving
// between locations need no edit here.
//
// Mutually exclusive with endpoint, connector and instance.
//
// +kubebuilder:validation:Optional
NetworkService *NetworkServiceBackendRef `json:"networkService,omitempty"`

// TLS contains backend TLS configuration.
//
// When the backend endpoint uses HTTPS with an IP address, the Hostname field
// must be specified for TLS certificate validation.
//
// Not supported for networkService backends, which are always reached over
// plaintext HTTP.
//
// +kubebuilder:validation:Optional
TLS *HTTPProxyBackendTLS `json:"tls,omitempty"`

Expand Down Expand Up @@ -209,6 +223,28 @@ type InstanceBackendRef struct {
Port int32 `json:"port"`
}

// NetworkServiceBackendRef references a NetworkService, and one of the ports it
// declares, as the backend of a rule.
type NetworkServiceBackendRef struct {
// Name of the referenced NetworkService. Must exist in the same namespace as
// this HTTPProxy.
//
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Name string `json:"name"`

// Port names a port declared in the referenced service's spec.ports, rather
// than giving a number, so the reference survives a change to the port the
// members answer on.
//
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
Port string `json:"port"`
}

// ConnectorReference references a Connector by name.
type ConnectorReference struct {
// Name of the referenced Connector.
Expand Down Expand Up @@ -457,6 +493,17 @@ const (
// references an EndpointSlice that does not exist.
HTTPProxyReasonInstanceBackendNotFound = "InstanceBackendNotFound"

// HTTPProxyReasonNetworkServiceBackendNotFound indicates that a
// networkService backend references a NetworkService that does not exist, or
// a port name that service does not declare.
HTTPProxyReasonNetworkServiceBackendNotFound = "NetworkServiceBackendNotFound"

// HTTPProxyReasonNetworkServiceMembersUnreferenced indicates that a
// networkService backend resolved more members than a single EndpointSlice
// holds. Every member is published, but only the members in the referenced
// slice are being served.
HTTPProxyReasonNetworkServiceMembersUnreferenced = "NetworkServiceMembersUnreferenced"

// This reason is used with the "Accepted" and "Programmed"
// conditions when the status is "Unknown" and no controller has reconciled
// the HTTPProxy.
Expand Down
39 changes: 38 additions & 1 deletion api/v1alpha/networkinterface_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,40 @@ const (
// created, so anything that withholds the workload until it is true waits for
// something its own waiting prevents.
NetworkInterfaceProgrammed = "Programmed"

// NetworkInterfaceHolderAvailable reports that whatever holds this interface
// says it is available to serve. It is written by the holder named in the
// held-by label, and the networking operator has no idea what a holder is.
//
// The operator only ever writes it Unknown: it seeds it when it creates the
// interface, and takes the departed holder's word back when a retained
// interface is unbound. True and False are the holder's alone.
//
// This is the condition a service reads to decide whether a member takes
// traffic. It says nothing about the interface: an interface with every
// address allocated and programmed still carries this false while whatever
// is behind it is starting, failing, or shutting down.
//
// Unrelated to status.phase, which reports whether a claim holds the
// interface at all. A phase of Available means no claim holds it, which is
// the opposite of anything being available to serve.
NetworkInterfaceHolderAvailable = "HolderAvailable"
)

const (
// NetworkInterfaceReasonHolderAvailable is what a holder reports on
// HolderAvailable once it is serving.
NetworkInterfaceReasonHolderAvailable = "HolderAvailable"

// NetworkInterfaceReasonHolderUnavailable is what a holder reports on
// HolderAvailable while it is starting, failing, or shutting down.
NetworkInterfaceReasonHolderUnavailable = "HolderUnavailable"

// NetworkInterfaceReasonHolderReleased is the operator taking back a
// departed holder's word when a retained interface is unbound. A retained
// interface outlives its holder, and the next claim of its name is a
// different holder that has said nothing yet.
NetworkInterfaceReasonHolderReleased = "HolderReleased"
)

// NetworkInterfaceAddress is an address the interface holds inside its network.
Expand Down Expand Up @@ -327,6 +361,8 @@ type NetworkInterfaceStatus struct {
// conditions report the current state of the interface. Allocated means every
// address is held. Prepared means the data plane is ready for a workload to
// consume it. Programmed means the data plane carries the addresses.
// HolderAvailable means whatever holds the interface reports itself available
// to serve, and it is the only one of the four a service reads.
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

Expand All @@ -351,14 +387,15 @@ type NetworkInterfaceStatus struct {
// +kubebuilder:printcolumn:name="Allocated",type=string,JSONPath=`.status.conditions[?(@.type=="Allocated")].status`
// +kubebuilder:printcolumn:name="Prepared",type=string,JSONPath=`.status.conditions[?(@.type=="Prepared")].status`
// +kubebuilder:printcolumn:name="Programmed",type=string,JSONPath=`.status.conditions[?(@.type=="Programmed")].status`
// +kubebuilder:printcolumn:name="HolderAvailable",type=string,JSONPath=`.status.conditions[?(@.type=="HolderAvailable")].status`
type NetworkInterface struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

// +kubebuilder:validation:Required
Spec NetworkInterfaceSpec `json:"spec,omitempty"`

// +kubebuilder:default={conditions:{{type:"Allocated",status:"Unknown",reason:"Pending", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"},{type:"Prepared",status:"Unknown",reason:"Pending", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"},{type:"Programmed",status:"Unknown",reason:"Pending", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"}}}
// +kubebuilder:default={conditions:{{type:"Allocated",status:"Unknown",reason:"Pending", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"},{type:"Prepared",status:"Unknown",reason:"Pending", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"},{type:"Programmed",status:"Unknown",reason:"Pending", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"},{type:"HolderAvailable",status:"Unknown",reason:"Pending", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"}}}
Status NetworkInterfaceStatus `json:"status,omitempty"`
}

Expand Down
Loading
Loading