diff --git a/api/v1alpha1/vpcattachment_types.go b/api/v1alpha1/vpcattachment_types.go index 0c417f6..8115c26 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,61 @@ 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"` +} + // IPAddress is an IPv4 or IPv6 address with CIDR notation. // +kubebuilder:validation:MaxLength=64 type IPAddress 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 ( + // VPCAttachmentInterfaceModeNetns moves the interface into the workload's + // network namespace, which is what a container consumes. + VPCAttachmentInterfaceModeNetns VPCAttachmentInterfaceMode = "Netns" + + // 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. // -// +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 + // 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"` + + // 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 +121,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..b587ed3 100644 --- a/config/crd/cloud.datumapis.com_vpcattachments.yaml +++ b/config/crd/cloud.datumapis.com_vpcattachments.yaml @@ -43,27 +43,45 @@ 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 + mode: + default: Netns + 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 + type: string name: default: eth0 description: Name of the interface (e.g., eth0). 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 + required: + - name + type: object vpc: description: VPC this attachment belongs to. properties: @@ -78,9 +96,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 +164,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 +187,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 +204,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..6d0764a 100644 --- a/docs/api/vpc.md +++ b/docs/api/vpc.md @@ -42,6 +42,23 @@ _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
| + + #### VPC @@ -99,7 +116,28 @@ _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
| +| `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
| + + +#### VPCAttachmentInterfaceMode + +_Underlying type:_ _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. + +_Validation:_ +- Enum: [Netns Hypervisor] + +_Appears in:_ +- [VPCAttachmentInterface](#vpcattachmentinterface) + +| Field | Description | +| --- | --- | +| `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 @@ -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..e683b1f 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 + 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 mode defaulted to Netns" + + - name: reject-vpcattachment-invalid-interface-mode + 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 + mode: tap + 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 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: empty addresses array correctly rejected" + echo "OK: unsupported interface mode correctly rejected" echo "Server response: $OUTPUT" - name: reject-vpcattachment-missing-vpc