From a9683fd9fe649ee91f7136b278dd8ea56ec521ed Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 20 Aug 2026 16:27:23 -0500 Subject: [PATCH 1/4] feat(api): make VPCAttachment usable as the interface-to-data-plane bridge Status becomes partially writable, the spec gains a NetworkInterface back-reference and a master-plugin selector, and a guest that manages its own addressing is now representable. Co-Authored-By: Claude Opus 5 (1M context) --- api/v1alpha1/vpcattachment_types.go | 98 +++++++++++++++---- api/v1alpha1/zz_generated.deepcopy.go | 20 ++++ .../cloud.datumapis.com_vpcattachments.yaml | 57 +++++++---- docs/api/vpc.md | 53 +++++++++- .../tests/vpc-crd-schema/chainsaw-test.yaml | 42 +++++++- 5 files changed, 221 insertions(+), 49 deletions(-) diff --git a/api/v1alpha1/vpcattachment_types.go b/api/v1alpha1/vpcattachment_types.go index 0c417f6..4a8d5eb 100644 --- a/api/v1alpha1/vpcattachment_types.go +++ b/api/v1alpha1/vpcattachment_types.go @@ -23,14 +23,25 @@ import ( const VPCAttachmentAnnotation = "k8s.v1alpha1.cloud.datumapis.com/vpc-attachment" +const ( + // ConditionTypeReady reports that identifiers are allocated and the + // NetworkAttachmentDefinition is written. + ConditionTypeReady = "Ready" + + // ConditionTypeProgrammed reports that the data plane realized the attachment. + ConditionTypeProgrammed = "Programmed" +) + // VPCAttachmentSpec defines the desired state of VPCAttachment -// -// +kubebuilder:validation:XValidation:rule="has(self.vpc) && self.vpc.name != ”",message="vpc reference is required" type VPCAttachmentSpec struct { // VPC this attachment belongs to. // +required VPC VPCRef `json:"vpc"` + // NetworkInterface this attachment realizes. + // +optional + InterfaceRef *NetworkInterfaceRef `json:"interfaceRef,omitempty"` + // Interface defines the network interface configuration. // +required Interface VPCAttachmentInterface `json:"interface"` @@ -43,27 +54,63 @@ type VPCRef struct { Name string `json:"name"` } +// NetworkInterfaceRef references a networking.datumapis.com NetworkInterface in +// the same namespace. +type NetworkInterfaceRef struct { + // Name of the NetworkInterface. + // +kubebuilder:validation:MinLength=1 + // +required + Name string `json:"name"` + + // UID disambiguates the reference across recreation of the same name. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=64 + // +optional + UID string `json:"uid,omitempty"` +} + // IPAddress is an IPv4 or IPv6 address with CIDR notation. // +kubebuilder:validation:MaxLength=64 type IPAddress string +// VPCAttachmentInterfaceType selects the CNI master plugin that realizes the +// interface. +// +kubebuilder:validation:Enum=veth;tap +type VPCAttachmentInterfaceType string + +const ( + // VPCAttachmentInterfaceTypeVeth attaches a container through galactic-veth. + VPCAttachmentInterfaceTypeVeth VPCAttachmentInterfaceType = "veth" + + // VPCAttachmentInterfaceTypeTap attaches a virtual machine guest through galactic-tap. + VPCAttachmentInterfaceTypeTap VPCAttachmentInterfaceType = "tap" +) + // VPCAttachmentInterface defines the network interface details. // -// +kubebuilder:validation:XValidation:rule="self.addresses.all(a, isCIDR(a))",message="each address must be a valid IPv4 or IPv6 CIDR" +// +kubebuilder:validation:XValidation:rule="!has(self.addresses) || self.addresses.all(a, isCIDR(a))",message="each address must be a valid IPv4 or IPv6 CIDR" type VPCAttachmentInterface struct { // Name of the interface (e.g., eth0). // +required // +default:value="eth0" Name string `json:"name"` - // A list of IPv4 or IPv6 addresses associated with the interface. - // +kubebuilder:validation:MinItems=1 + // Type of interface to create, which selects the CNI master plugin. + // +kubebuilder:default=veth + // +optional + Type VPCAttachmentInterfaceType `json:"type,omitempty"` + + // A list of IPv4 or IPv6 addresses associated with the interface. Empty when + // the guest manages its own addressing. // +kubebuilder:validation:MaxItems=16 - // +required - Addresses []IPAddress `json:"addresses"` + // +optional + Addresses []IPAddress `json:"addresses,omitempty"` } // VPCAttachmentStatus defines the observed state of VPCAttachment. +// +// Every field but Conditions is optional: an identifier is recorded before a pod +// attaches, and a guest managing its own addressing never reports a subnet. type VPCAttachmentStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` @@ -76,44 +123,57 @@ type VPCAttachmentStatus struct { // Base62-encoded VPC identifier. // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=16 - VPC string `json:"vpc"` + // +optional + VPC string `json:"vpc,omitempty"` // Base62-encoded VPCAttachment identifier. // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=16 - VPCAttachment string `json:"vpcAttachment"` + // +optional + VPCAttachment string `json:"vpcAttachment,omitempty"` // Kubernetes node name where the attachment lives. // +kubebuilder:validation:MinLength=1 - Node string `json:"node"` + // +optional + Node string `json:"node,omitempty"` // Full container ID (46 hex characters). // +kubebuilder:validation:MinLength=46 // +kubebuilder:validation:MaxLength=46 - ContainerID string `json:"containerID"` + // +optional + ContainerID string `json:"containerID,omitempty"` // Pod name. // +kubebuilder:validation:MinLength=1 - PodName string `json:"podName"` + // +optional + PodName string `json:"podName,omitempty"` - // Host-side veth device name (e.g., "G000000010010H"). + // Host-side veth or tap device name (e.g., "G000000010013H"). // +kubebuilder:validation:MinLength=1 - HostInterface string `json:"hostInterface"` + // +optional + HostInterface string `json:"hostInterface,omitempty"` - // VRF device name (e.g., "G000000010010V"). + // VRF device name, which is per-VPC (e.g., "G000000010V"). // +kubebuilder:validation:MinLength=1 - VRFInterface string `json:"vrfInterface"` + // +optional + VRFInterface string `json:"vrfInterface,omitempty"` - // Guest-side veth device name (e.g., "G000000010010G"). + // Guest-side veth device name (e.g., "G000000010013G"). // +kubebuilder:validation:MinLength=1 // +optional GuestInterface string `json:"guestInterface,omitempty"` - // Allocated /80 subnet in CIDR notation (e.g., "fd00:10:ff01:0:1::/80"). + // Allocated subnet in CIDR notation (e.g., "fd00:10:ff01:0:1::/80"). // +kubebuilder:validation:MinLength=1 + // +optional // // +kubebuilder:validation:XValidation:rule="isCIDR(self)",message="podSubnet must be a valid IPv6 CIDR" - PodSubnet string `json:"podSubnet"` + PodSubnet string `json:"podSubnet,omitempty"` + + // NetworkAttachmentDefinition rendered for this attachment. + // +kubebuilder:validation:MinLength=1 + // +optional + NetworkAttachmentDefinition string `json:"networkAttachmentDefinition,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0702675..4bc77cf 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -26,6 +26,21 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkInterfaceRef) DeepCopyInto(out *NetworkInterfaceRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkInterfaceRef. +func (in *NetworkInterfaceRef) DeepCopy() *NetworkInterfaceRef { + if in == nil { + return nil + } + out := new(NetworkInterfaceRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VPC) DeepCopyInto(out *VPC) { *out = *in @@ -136,6 +151,11 @@ func (in *VPCAttachmentList) DeepCopyObject() runtime.Object { func (in *VPCAttachmentSpec) DeepCopyInto(out *VPCAttachmentSpec) { *out = *in out.VPC = in.VPC + if in.InterfaceRef != nil { + in, out := &in.InterfaceRef, &out.InterfaceRef + *out = new(NetworkInterfaceRef) + **out = **in + } in.Interface.DeepCopyInto(&out.Interface) } diff --git a/config/crd/cloud.datumapis.com_vpcattachments.yaml b/config/crd/cloud.datumapis.com_vpcattachments.yaml index d451673..c3e7a3f 100644 --- a/config/crd/cloud.datumapis.com_vpcattachments.yaml +++ b/config/crd/cloud.datumapis.com_vpcattachments.yaml @@ -43,27 +43,50 @@ spec: description: Interface defines the network interface configuration. properties: addresses: - description: A list of IPv4 or IPv6 addresses associated with - the interface. + description: |- + A list of IPv4 or IPv6 addresses associated with the interface. Empty when + the guest manages its own addressing. items: description: IPAddress is an IPv4 or IPv6 address with CIDR notation. maxLength: 64 type: string maxItems: 16 - minItems: 1 type: array name: default: eth0 description: Name of the interface (e.g., eth0). type: string + type: + default: veth + description: Type of interface to create, which selects the CNI + master plugin. + enum: + - veth + - tap + type: string required: - - addresses - name type: object x-kubernetes-validations: - message: each address must be a valid IPv4 or IPv6 CIDR - rule: self.addresses.all(a, isCIDR(a)) + rule: '!has(self.addresses) || self.addresses.all(a, isCIDR(a))' + interfaceRef: + description: NetworkInterface this attachment realizes. + properties: + name: + description: Name of the NetworkInterface. + minLength: 1 + type: string + uid: + description: UID disambiguates the reference across recreation + of the same name. + maxLength: 64 + minLength: 1 + type: string + required: + - name + type: object vpc: description: VPC this attachment belongs to. properties: @@ -78,9 +101,6 @@ spec: - interface - vpc type: object - x-kubernetes-validations: - - message: vpc reference is required - rule: has(self.vpc) && self.vpc.name != '' status: description: status defines the observed state of VPCAttachment properties: @@ -149,11 +169,15 @@ spec: minLength: 46 type: string guestInterface: - description: Guest-side veth device name (e.g., "G000000010010G"). + description: Guest-side veth device name (e.g., "G000000010013G"). minLength: 1 type: string hostInterface: - description: Host-side veth device name (e.g., "G000000010010H"). + description: Host-side veth or tap device name (e.g., "G000000010013H"). + minLength: 1 + type: string + networkAttachmentDefinition: + description: NetworkAttachmentDefinition rendered for this attachment. minLength: 1 type: string node: @@ -168,7 +192,7 @@ spec: minLength: 1 type: string podSubnet: - description: Allocated /80 subnet in CIDR notation (e.g., "fd00:10:ff01:0:1::/80"). + description: Allocated subnet in CIDR notation (e.g., "fd00:10:ff01:0:1::/80"). minLength: 1 type: string x-kubernetes-validations: @@ -185,18 +209,9 @@ spec: minLength: 1 type: string vrfInterface: - description: VRF device name (e.g., "G000000010010V"). + description: VRF device name, which is per-VPC (e.g., "G000000010V"). minLength: 1 type: string - required: - - containerID - - hostInterface - - node - - podName - - podSubnet - - vpc - - vpcAttachment - - vrfInterface type: object required: - spec diff --git a/docs/api/vpc.md b/docs/api/vpc.md index d0588b6..9a2ed01 100644 --- a/docs/api/vpc.md +++ b/docs/api/vpc.md @@ -42,6 +42,24 @@ _Appears in:_ +#### NetworkInterfaceRef + + + +NetworkInterfaceRef references a networking.datumapis.com NetworkInterface in +the same namespace. + + + +_Appears in:_ +- [VPCAttachmentSpec](#vpcattachmentspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | Name of the NetworkInterface. | | MinLength: 1
| +| `uid` _string_ | UID disambiguates the reference across recreation of the same name. | | MaxLength: 64
MinLength: 1
| + + #### VPC @@ -99,7 +117,27 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _string_ | Name of the interface (e.g., eth0). | | | -| `addresses` _[IPAddress](#ipaddress) array_ | A list of IPv4 or IPv6 addresses associated with the interface. | | MaxItems: 16
MaxLength: 64
MinItems: 1
| +| `type` _[VPCAttachmentInterfaceType](#vpcattachmentinterfacetype)_ | Type of interface to create, which selects the CNI master plugin. | veth | Enum: [veth tap]
| +| `addresses` _[IPAddress](#ipaddress) array_ | A list of IPv4 or IPv6 addresses associated with the interface. Empty when
the guest manages its own addressing. | | MaxItems: 16
MaxLength: 64
| + + +#### VPCAttachmentInterfaceType + +_Underlying type:_ _string_ + +VPCAttachmentInterfaceType selects the CNI master plugin that realizes the +interface. + +_Validation:_ +- Enum: [veth tap] + +_Appears in:_ +- [VPCAttachmentInterface](#vpcattachmentinterface) + +| Field | Description | +| --- | --- | +| `veth` | VPCAttachmentInterfaceTypeVeth attaches a container through galactic-veth.
| +| `tap` | VPCAttachmentInterfaceTypeTap attaches a virtual machine guest through galactic-tap.
| #### VPCAttachmentSpec @@ -116,6 +154,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `vpc` _[VPCRef](#vpcref)_ | VPC this attachment belongs to. | | | +| `interfaceRef` _[NetworkInterfaceRef](#networkinterfaceref)_ | NetworkInterface this attachment realizes. | | | | `interface` _[VPCAttachmentInterface](#vpcattachmentinterface)_ | Interface defines the network interface configuration. | | | @@ -125,6 +164,9 @@ _Appears in:_ VPCAttachmentStatus defines the observed state of VPCAttachment. +Every field but Conditions is optional: an identifier is recorded before a pod +attaches, and a guest managing its own addressing never reports a subnet. + _Appears in:_ @@ -139,10 +181,11 @@ _Appears in:_ | `node` _string_ | Kubernetes node name where the attachment lives. | | MinLength: 1
| | `containerID` _string_ | Full container ID (46 hex characters). | | MaxLength: 46
MinLength: 46
| | `podName` _string_ | Pod name. | | MinLength: 1
| -| `hostInterface` _string_ | Host-side veth device name (e.g., "G000000010010H"). | | MinLength: 1
| -| `vrfInterface` _string_ | VRF device name (e.g., "G000000010010V"). | | MinLength: 1
| -| `guestInterface` _string_ | Guest-side veth device name (e.g., "G000000010010G"). | | MinLength: 1
| -| `podSubnet` _string_ | Allocated /80 subnet in CIDR notation (e.g., "fd00:10:ff01:0:1::/80"). | | MinLength: 1
| +| `hostInterface` _string_ | Host-side veth or tap device name (e.g., "G000000010013H"). | | MinLength: 1
| +| `vrfInterface` _string_ | VRF device name, which is per-VPC (e.g., "G000000010V"). | | MinLength: 1
| +| `guestInterface` _string_ | Guest-side veth device name (e.g., "G000000010013G"). | | MinLength: 1
| +| `podSubnet` _string_ | Allocated subnet in CIDR notation (e.g., "fd00:10:ff01:0:1::/80"). | | MinLength: 1
| +| `networkAttachmentDefinition` _string_ | NetworkAttachmentDefinition rendered for this attachment. | | MinLength: 1
| #### VPCRef diff --git a/test/e2e/tests/vpc-crd-schema/chainsaw-test.yaml b/test/e2e/tests/vpc-crd-schema/chainsaw-test.yaml index ad13353..4ea4688 100644 --- a/test/e2e/tests/vpc-crd-schema/chainsaw-test.yaml +++ b/test/e2e/tests/vpc-crd-schema/chainsaw-test.yaml @@ -182,7 +182,7 @@ spec: fi echo "OK: interface name defaulted to eth0" - - name: reject-vpcattachment-empty-addresses + - name: accept-vpcattachment-empty-addresses try: - script: content: | @@ -191,7 +191,7 @@ spec: apiVersion: cloud.datumapis.com/v1alpha1 kind: VPCAttachment metadata: - name: e2e-invalid-attachment + name: e2e-no-addressing-attachment spec: vpc: name: e2e-valid-vpc @@ -202,12 +202,46 @@ spec: ) EXIT=$? set -e + if [ "$EXIT" -ne 0 ]; then + echo "ERROR: expected a guest managing its own addressing to be accepted" + echo "Server response: $OUTPUT" + exit 1 + fi + TYPE=$(kubectl get vpcattachments.cloud.datumapis.com e2e-no-addressing-attachment \ + -n "$NAMESPACE" -o jsonpath='{.spec.interface.type}') + if [ "$TYPE" != "veth" ]; then + echo "ERROR: expected interface type default 'veth' but got '$TYPE'" + exit 1 + fi + kubectl delete vpcattachments.cloud.datumapis.com e2e-no-addressing-attachment -n "$NAMESPACE" + echo "OK: empty addresses accepted, interface type defaulted to veth" + + - name: reject-vpcattachment-invalid-interface-type + try: + - script: + content: | + set +e + OUTPUT=$(kubectl apply -n "$NAMESPACE" -f - 2>&1 <<'EOF' + apiVersion: cloud.datumapis.com/v1alpha1 + kind: VPCAttachment + metadata: + name: e2e-invalid-attachment + spec: + vpc: + name: e2e-valid-vpc + interface: + name: eth0 + type: macvlan + EOF + ) + EXIT=$? + set -e if [ "$EXIT" -eq 0 ]; then - echo "ERROR: expected rejection of empty addresses array but resource was accepted" + echo "ERROR: expected rejection of unsupported interface type but resource was accepted" kubectl delete vpcattachments.cloud.datumapis.com e2e-invalid-attachment -n "$NAMESPACE" 2>/dev/null || true exit 1 fi - echo "OK: empty addresses array correctly rejected" + echo "OK: unsupported interface type correctly rejected" echo "Server response: $OUTPUT" - name: reject-vpcattachment-missing-vpc From dff35859f56df450d2fd8d34dead3d126a3056e7 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 20 Aug 2026 17:20:10 -0500 Subject: [PATCH 2/4] refactor(api): describe how a guest consumes an interface, not which plugin interface.mode (Netns | Hypervisor) replaces interface.type (veth | tap), so galactic's plugin names stay out of a cloud-level API, and interfaceRef.uid becomes load-bearing: a mismatch means the attachment is stale. Co-Authored-By: Claude Opus 5 (1M context) --- api/v1alpha1/vpcattachment_types.go | 28 +++++++++++-------- .../cloud.datumapis.com_vpcattachments.yaml | 20 ++++++------- docs/api/vpc.md | 17 +++++------ .../tests/vpc-crd-schema/chainsaw-test.yaml | 18 ++++++------ 4 files changed, 44 insertions(+), 39 deletions(-) diff --git a/api/v1alpha1/vpcattachment_types.go b/api/v1alpha1/vpcattachment_types.go index 4a8d5eb..cece637 100644 --- a/api/v1alpha1/vpcattachment_types.go +++ b/api/v1alpha1/vpcattachment_types.go @@ -62,7 +62,8 @@ type NetworkInterfaceRef struct { // +required Name string `json:"name"` - // UID disambiguates the reference across recreation of the same name. + // UID of the NetworkInterface. When set, a controller that finds a different + // UID must treat the attachment as stale rather than bind to the new interface. // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=64 // +optional @@ -73,17 +74,20 @@ type NetworkInterfaceRef struct { // +kubebuilder:validation:MaxLength=64 type IPAddress string -// VPCAttachmentInterfaceType selects the CNI master plugin that realizes the -// interface. -// +kubebuilder:validation:Enum=veth;tap -type VPCAttachmentInterfaceType string +// VPCAttachmentInterfaceMode is how the workload consumes the interface. It +// describes the guest, not the data plane, so a change of implementation on the +// data plane side does not move this API. +// +kubebuilder:validation:Enum=Netns;Hypervisor +type VPCAttachmentInterfaceMode string const ( - // VPCAttachmentInterfaceTypeVeth attaches a container through galactic-veth. - VPCAttachmentInterfaceTypeVeth VPCAttachmentInterfaceType = "veth" + // VPCAttachmentInterfaceModeNetns moves the interface into the workload's + // network namespace, which is what a container consumes. + VPCAttachmentInterfaceModeNetns VPCAttachmentInterfaceMode = "Netns" - // VPCAttachmentInterfaceTypeTap attaches a virtual machine guest through galactic-tap. - VPCAttachmentInterfaceTypeTap VPCAttachmentInterfaceType = "tap" + // VPCAttachmentInterfaceModeHypervisor hands the interface to a hypervisor as + // a device, which is what a virtual machine guest consumes. + VPCAttachmentInterfaceModeHypervisor VPCAttachmentInterfaceMode = "Hypervisor" ) // VPCAttachmentInterface defines the network interface details. @@ -95,10 +99,10 @@ type VPCAttachmentInterface struct { // +default:value="eth0" Name string `json:"name"` - // Type of interface to create, which selects the CNI master plugin. - // +kubebuilder:default=veth + // Mode is how the workload consumes the interface. + // +kubebuilder:default=Netns // +optional - Type VPCAttachmentInterfaceType `json:"type,omitempty"` + Mode VPCAttachmentInterfaceMode `json:"mode,omitempty"` // A list of IPv4 or IPv6 addresses associated with the interface. Empty when // the guest manages its own addressing. diff --git a/config/crd/cloud.datumapis.com_vpcattachments.yaml b/config/crd/cloud.datumapis.com_vpcattachments.yaml index c3e7a3f..bbb2cb6 100644 --- a/config/crd/cloud.datumapis.com_vpcattachments.yaml +++ b/config/crd/cloud.datumapis.com_vpcattachments.yaml @@ -53,18 +53,17 @@ spec: type: string maxItems: 16 type: array + mode: + default: Netns + description: Mode is how the workload consumes the interface. + enum: + - Netns + - Hypervisor + type: string name: default: eth0 description: Name of the interface (e.g., eth0). type: string - type: - default: veth - description: Type of interface to create, which selects the CNI - master plugin. - enum: - - veth - - tap - type: string required: - name type: object @@ -79,8 +78,9 @@ spec: minLength: 1 type: string uid: - description: UID disambiguates the reference across recreation - of the same name. + description: |- + UID of the NetworkInterface. When set, a controller that finds a different + UID must treat the attachment as stale rather than bind to the new interface. maxLength: 64 minLength: 1 type: string diff --git a/docs/api/vpc.md b/docs/api/vpc.md index 9a2ed01..d21a69b 100644 --- a/docs/api/vpc.md +++ b/docs/api/vpc.md @@ -57,7 +57,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _string_ | Name of the NetworkInterface. | | MinLength: 1
| -| `uid` _string_ | UID disambiguates the reference across recreation of the same name. | | MaxLength: 64
MinLength: 1
| +| `uid` _string_ | UID of the NetworkInterface. When set, a controller that finds a different
UID must treat the attachment as stale rather than bind to the new interface. | | MaxLength: 64
MinLength: 1
| #### VPC @@ -117,27 +117,28 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _string_ | Name of the interface (e.g., eth0). | | | -| `type` _[VPCAttachmentInterfaceType](#vpcattachmentinterfacetype)_ | Type of interface to create, which selects the CNI master plugin. | veth | Enum: [veth tap]
| +| `mode` _[VPCAttachmentInterfaceMode](#vpcattachmentinterfacemode)_ | Mode is how the workload consumes the interface. | Netns | Enum: [Netns Hypervisor]
| | `addresses` _[IPAddress](#ipaddress) array_ | A list of IPv4 or IPv6 addresses associated with the interface. Empty when
the guest manages its own addressing. | | MaxItems: 16
MaxLength: 64
| -#### VPCAttachmentInterfaceType +#### VPCAttachmentInterfaceMode _Underlying type:_ _string_ -VPCAttachmentInterfaceType selects the CNI master plugin that realizes the -interface. +VPCAttachmentInterfaceMode is how the workload consumes the interface. It +describes the guest, not the data plane, so a change of implementation on the +data plane side does not move this API. _Validation:_ -- Enum: [veth tap] +- Enum: [Netns Hypervisor] _Appears in:_ - [VPCAttachmentInterface](#vpcattachmentinterface) | Field | Description | | --- | --- | -| `veth` | VPCAttachmentInterfaceTypeVeth attaches a container through galactic-veth.
| -| `tap` | VPCAttachmentInterfaceTypeTap attaches a virtual machine guest through galactic-tap.
| +| `Netns` | VPCAttachmentInterfaceModeNetns moves the interface into the workload's
network namespace, which is what a container consumes.
| +| `Hypervisor` | VPCAttachmentInterfaceModeHypervisor hands the interface to a hypervisor as
a device, which is what a virtual machine guest consumes.
| #### VPCAttachmentSpec diff --git a/test/e2e/tests/vpc-crd-schema/chainsaw-test.yaml b/test/e2e/tests/vpc-crd-schema/chainsaw-test.yaml index 4ea4688..e683b1f 100644 --- a/test/e2e/tests/vpc-crd-schema/chainsaw-test.yaml +++ b/test/e2e/tests/vpc-crd-schema/chainsaw-test.yaml @@ -207,16 +207,16 @@ spec: echo "Server response: $OUTPUT" exit 1 fi - TYPE=$(kubectl get vpcattachments.cloud.datumapis.com e2e-no-addressing-attachment \ - -n "$NAMESPACE" -o jsonpath='{.spec.interface.type}') - if [ "$TYPE" != "veth" ]; then - echo "ERROR: expected interface type default 'veth' but got '$TYPE'" + MODE=$(kubectl get vpcattachments.cloud.datumapis.com e2e-no-addressing-attachment \ + -n "$NAMESPACE" -o jsonpath='{.spec.interface.mode}') + if [ "$MODE" != "Netns" ]; then + echo "ERROR: expected interface mode default 'Netns' but got '$MODE'" exit 1 fi kubectl delete vpcattachments.cloud.datumapis.com e2e-no-addressing-attachment -n "$NAMESPACE" - echo "OK: empty addresses accepted, interface type defaulted to veth" + echo "OK: empty addresses accepted, interface mode defaulted to Netns" - - name: reject-vpcattachment-invalid-interface-type + - name: reject-vpcattachment-invalid-interface-mode try: - script: content: | @@ -231,17 +231,17 @@ spec: name: e2e-valid-vpc interface: name: eth0 - type: macvlan + mode: tap EOF ) EXIT=$? set -e if [ "$EXIT" -eq 0 ]; then - echo "ERROR: expected rejection of unsupported interface type but resource was accepted" + echo "ERROR: expected rejection of unsupported interface mode but resource was accepted" kubectl delete vpcattachments.cloud.datumapis.com e2e-invalid-attachment -n "$NAMESPACE" 2>/dev/null || true exit 1 fi - echo "OK: unsupported interface type correctly rejected" + echo "OK: unsupported interface mode correctly rejected" echo "Server response: $OUTPUT" - name: reject-vpcattachment-missing-vpc From fd31fd50411563decbfb4812f8adb233eba15802 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 20 Aug 2026 18:13:19 -0500 Subject: [PATCH 3/4] docs(api): note the controller writes interface.mode Co-Authored-By: Claude Opus 5 (1M context) --- api/v1alpha1/vpcattachment_types.go | 3 ++- config/crd/cloud.datumapis.com_vpcattachments.yaml | 4 +++- docs/api/vpc.md | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/vpcattachment_types.go b/api/v1alpha1/vpcattachment_types.go index cece637..5d7c30c 100644 --- a/api/v1alpha1/vpcattachment_types.go +++ b/api/v1alpha1/vpcattachment_types.go @@ -99,7 +99,8 @@ type VPCAttachmentInterface struct { // +default:value="eth0" Name string `json:"name"` - // Mode is how the workload consumes the interface. + // Mode is how the workload consumes the interface, resolved and written by + // the attachment controller rather than by whoever runs the workload. // +kubebuilder:default=Netns // +optional Mode VPCAttachmentInterfaceMode `json:"mode,omitempty"` diff --git a/config/crd/cloud.datumapis.com_vpcattachments.yaml b/config/crd/cloud.datumapis.com_vpcattachments.yaml index bbb2cb6..c42fa4f 100644 --- a/config/crd/cloud.datumapis.com_vpcattachments.yaml +++ b/config/crd/cloud.datumapis.com_vpcattachments.yaml @@ -55,7 +55,9 @@ spec: type: array mode: default: Netns - description: Mode is how the workload consumes the interface. + description: |- + Mode is how the workload consumes the interface, resolved and written by + the attachment controller rather than by whoever runs the workload. enum: - Netns - Hypervisor diff --git a/docs/api/vpc.md b/docs/api/vpc.md index d21a69b..82a07eb 100644 --- a/docs/api/vpc.md +++ b/docs/api/vpc.md @@ -117,7 +117,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _string_ | Name of the interface (e.g., eth0). | | | -| `mode` _[VPCAttachmentInterfaceMode](#vpcattachmentinterfacemode)_ | Mode is how the workload consumes the interface. | Netns | Enum: [Netns Hypervisor]
| +| `mode` _[VPCAttachmentInterfaceMode](#vpcattachmentinterfacemode)_ | Mode is how the workload consumes the interface, resolved and written by
the attachment controller rather than by whoever runs the workload. | Netns | Enum: [Netns Hypervisor]
| | `addresses` _[IPAddress](#ipaddress) array_ | A list of IPv4 or IPv6 addresses associated with the interface. Empty when
the guest manages its own addressing. | | MaxItems: 16
MaxLength: 64
| From e230e9056f5541577ad12f0dad8043c2bd3065a2 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Thu, 20 Aug 2026 19:56:56 -0500 Subject: [PATCH 4/4] refactor(api): drop interfaceRef.uid in favour of the owner reference The controller that writes interfaceRef also sets an owner reference carrying the interface's UID, so the field restated a fact the object already held and owner-reference garbage collection already acts on. Co-Authored-By: Claude Opus 5 (1M context) --- api/v1alpha1/vpcattachment_types.go | 7 ------- config/crd/cloud.datumapis.com_vpcattachments.yaml | 7 ------- docs/api/vpc.md | 1 - 3 files changed, 15 deletions(-) diff --git a/api/v1alpha1/vpcattachment_types.go b/api/v1alpha1/vpcattachment_types.go index 5d7c30c..8115c26 100644 --- a/api/v1alpha1/vpcattachment_types.go +++ b/api/v1alpha1/vpcattachment_types.go @@ -61,13 +61,6 @@ type NetworkInterfaceRef struct { // +kubebuilder:validation:MinLength=1 // +required Name string `json:"name"` - - // UID of the NetworkInterface. When set, a controller that finds a different - // UID must treat the attachment as stale rather than bind to the new interface. - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=64 - // +optional - UID string `json:"uid,omitempty"` } // IPAddress is an IPv4 or IPv6 address with CIDR notation. diff --git a/config/crd/cloud.datumapis.com_vpcattachments.yaml b/config/crd/cloud.datumapis.com_vpcattachments.yaml index c42fa4f..b587ed3 100644 --- a/config/crd/cloud.datumapis.com_vpcattachments.yaml +++ b/config/crd/cloud.datumapis.com_vpcattachments.yaml @@ -79,13 +79,6 @@ spec: description: Name of the NetworkInterface. minLength: 1 type: string - uid: - description: |- - UID of the NetworkInterface. When set, a controller that finds a different - UID must treat the attachment as stale rather than bind to the new interface. - maxLength: 64 - minLength: 1 - type: string required: - name type: object diff --git a/docs/api/vpc.md b/docs/api/vpc.md index 82a07eb..6d0764a 100644 --- a/docs/api/vpc.md +++ b/docs/api/vpc.md @@ -57,7 +57,6 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `name` _string_ | Name of the NetworkInterface. | | MinLength: 1
| -| `uid` _string_ | UID of the NetworkInterface. When set, a controller that finds a different
UID must treat the attachment as stale rather than bind to the new interface. | | MaxLength: 64
MinLength: 1
| #### VPC