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