From df32177141b20426d21418ad12a69e08ebe4a796 Mon Sep 17 00:00:00 2001 From: Ian Gann Date: Thu, 9 Jul 2026 11:31:11 -0700 Subject: [PATCH 1/2] api: add DefaultNamespaceConfiguration override for vpc provider Adds an optional defaultNamespaceConfiguration field to NetworkProviderEntry, letting the vpc provider's namespace-onboarding defaults (currently just privateCIDRs) be changed independently of the append-only/immutable systemConfiguration.vpcConfig, per the design spec (docs/superpowers/specs/2026-06-30-default-namespace-configuration-design.md). NetworkProviderDefaultConfig is a value type with omitzero (not a pointer) to satisfy KAL's optionalfields check, now that the controller-tools bump makes required-field generation independent of that choice. This required guarding the new CEL rule's nested has() check at both levels (self.defaultNamespaceConfiguration and .vpcConfig) rather than just the leaf, since has() errors instead of returning false when the intermediate object is entirely absent from the wire representation (as it will be for any WNC that doesn't set defaultNamespaceConfiguration at all). CEL test coverage for the new field lives in test/cel/workloadnetworkconfiguration_test.go, appended to the ported suite: valid override, rejection on non-vpc types, MinProperties=1 and MinItems=1 rejection (via unstructured, since typed-client omitempty/omitzero would otherwise drop the very empty values under test), and full-replace admission (no append-only constraint, unlike the system config). --- .../namespacenetworkconfiguration_types.go | 35 +++++ .../workloadnetworkconfiguration_types.go | 14 ++ api/v1alpha1/zz_generated.deepcopy.go | 41 ++++++ test/cel/workloadnetworkconfiguration_test.go | 137 ++++++++++++++++++ 4 files changed, 227 insertions(+) diff --git a/api/v1alpha1/namespacenetworkconfiguration_types.go b/api/v1alpha1/namespacenetworkconfiguration_types.go index fe18001..468a164 100644 --- a/api/v1alpha1/namespacenetworkconfiguration_types.go +++ b/api/v1alpha1/namespacenetworkconfiguration_types.go @@ -197,6 +197,41 @@ type AutoCreateVPCConfig struct { PrivateCIDRs []string `json:"privateCIDRs,omitempty"` } +// NetworkProviderDefaultConfig holds the subset of provider configuration that +// may be independently overridden for new namespaces, distinct from and +// unconstrained by the append-only/immutable rules that apply to +// systemConfiguration. Currently only the vpc provider has such a divergence. +// +// +kubebuilder:validation:MinProperties=1 +type NetworkProviderDefaultConfig struct { + // vpcConfig overrides default values used when auto-creating VPCs for new + // namespaces under the vpc provider. + // + // +optional + VPCConfig *DefaultVPCConfig `json:"vpcConfig,omitempty"` +} + +// DefaultVPCConfig holds VPC default values that may be changed independently +// of the vpc provider's systemConfiguration. +type DefaultVPCConfig struct { + // privateCIDRs replaces the CIDR blocks used as private-subnet/private-pod-IP + // defaults for VPCs auto-created for namespaces onboarding after this is + // set. Already-created namespace VPCs are unaffected. + // + // This value fully replaces (not appends to) the previous default and is + // not subject to the append-only constraint that applies to + // systemConfiguration.vpcConfig.autoCreateConfig.privateCIDRs. When unset, + // new namespaces use systemConfiguration's privateCIDRs instead. + // + // +optional + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + // +kubebuilder:validation:items:MaxLength=64 + // +kubebuilder:validation:items:Pattern=`^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$` + // +listType=atomic + PrivateCIDRs []string `json:"privateCIDRs,omitempty"` +} + // VPCConfig specifies the VPC network configuration for a namespace. // // There are two mutually exclusive modes: diff --git a/api/v1alpha1/workloadnetworkconfiguration_types.go b/api/v1alpha1/workloadnetworkconfiguration_types.go index aa129ed..2d9b79b 100644 --- a/api/v1alpha1/workloadnetworkconfiguration_types.go +++ b/api/v1alpha1/workloadnetworkconfiguration_types.go @@ -36,6 +36,7 @@ const ( // +kubebuilder:validation:XValidation:rule="self.type == 'vsphere-distributed' || !has(self.systemConfiguration.vsphereDistributedConfig)",message="systemConfiguration.vsphereDistributedConfig may only be set when type is vsphere-distributed" // +kubebuilder:validation:XValidation:rule="self.type != 'vpc' || has(self.systemConfiguration.vpcConfig)",message="systemConfiguration.vpcConfig must be set when type is vpc" // +kubebuilder:validation:XValidation:rule="self.type == 'vpc' || !has(self.systemConfiguration.vpcConfig)",message="systemConfiguration.vpcConfig may only be set when type is vpc" +// +kubebuilder:validation:XValidation:rule="self.type == 'vpc' || !has(self.defaultNamespaceConfiguration) || !has(self.defaultNamespaceConfiguration.vpcConfig)",message="defaultNamespaceConfiguration.vpcConfig may only be set when type is vpc" // NetworkProviderEntry pairs a network provider type with its system-level // NamespaceNetworkConfiguration template. Exactly one entry per type is allowed @@ -50,6 +51,19 @@ type NetworkProviderEntry struct { // // +required SystemConfiguration *NamespaceNetworkConfig `json:"systemConfiguration,omitempty"` + + // defaultNamespaceConfiguration optionally overrides configuration values + // applied when provisioning new namespaces under this provider, going + // forward. When unset, or when a given sub-field is unset, the equivalent + // value from systemConfiguration is used instead. + // + // Unlike systemConfiguration, values here are not tied to the currently + // active system NNC: they may be replaced freely and only take effect for + // namespaces onboarded after the change. Namespaces already associated + // under this provider are unaffected. + // + // +optional + DefaultNamespaceConfiguration NetworkProviderDefaultConfig `json:"defaultNamespaceConfiguration,omitempty,omitzero"` } // +kubebuilder:validation:XValidation:rule="self.providers.exists(p, p.type == self.activeSystemProvider)",message="activeSystemProvider must reference a provider type declared in providers" diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 70d2424..b208fa2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -159,6 +159,26 @@ func (in *ClientSecretReference) DeepCopy() *ClientSecretReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DefaultVPCConfig) DeepCopyInto(out *DefaultVPCConfig) { + *out = *in + if in.PrivateCIDRs != nil { + in, out := &in.PrivateCIDRs, &out.PrivateCIDRs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultVPCConfig. +func (in *DefaultVPCConfig) DeepCopy() *DefaultVPCConfig { + if in == nil { + return nil + } + out := new(DefaultVPCConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FoundationLoadBalancerConfig) DeepCopyInto(out *FoundationLoadBalancerConfig) { *out = *in @@ -1325,6 +1345,26 @@ func (in *NetworkList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkProviderDefaultConfig) DeepCopyInto(out *NetworkProviderDefaultConfig) { + *out = *in + if in.VPCConfig != nil { + in, out := &in.VPCConfig, &out.VPCConfig + *out = new(DefaultVPCConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkProviderDefaultConfig. +func (in *NetworkProviderDefaultConfig) DeepCopy() *NetworkProviderDefaultConfig { + if in == nil { + return nil + } + out := new(NetworkProviderDefaultConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkProviderEntry) DeepCopyInto(out *NetworkProviderEntry) { *out = *in @@ -1333,6 +1373,7 @@ func (in *NetworkProviderEntry) DeepCopyInto(out *NetworkProviderEntry) { *out = new(NamespaceNetworkConfig) (*in).DeepCopyInto(*out) } + in.DefaultNamespaceConfiguration.DeepCopyInto(&out.DefaultNamespaceConfiguration) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkProviderEntry. diff --git a/test/cel/workloadnetworkconfiguration_test.go b/test/cel/workloadnetworkconfiguration_test.go index a1fa509..0c00699 100644 --- a/test/cel/workloadnetworkconfiguration_test.go +++ b/test/cel/workloadnetworkconfiguration_test.go @@ -401,3 +401,140 @@ func TestWorkloadNetworkConfiguration_ConditionStatusOutsideEnum_Rejected(t *tes t.Fatalf("expected rejection containing %q, got: %v", "Unsupported value", err) } } + +// ----------------------------------------------------------------------- +// defaultNamespaceConfiguration +// Rule: self.type == 'vpc' || !has(self.defaultNamespaceConfiguration.vpcConfig) +// NetworkProviderDefaultConfig: +kubebuilder:validation:MinProperties=1 +// DefaultVPCConfig.privateCIDRs: +optional, minItems=1, maxItems=16, CIDR pattern +// ----------------------------------------------------------------------- + +// vpcWNC builds a minimal valid vpc-provider WNC named "default". +func vpcWNC() *netv1alpha1.WorkloadNetworkConfiguration { + return &netv1alpha1.WorkloadNetworkConfiguration{ + ObjectMeta: metav1.ObjectMeta{Name: wncDefaultName}, + Spec: netv1alpha1.WorkloadNetworkConfigurationSpec{ + Providers: []netv1alpha1.NetworkProviderEntry{ + { + Type: netv1alpha1.NetworkProviderVPC, + SystemConfiguration: &netv1alpha1.NamespaceNetworkConfig{ + VPCConfig: netv1alpha1.VPCConfig{ + AutoCreateConfig: netv1alpha1.AutoCreateVPCConfig{ + NSXProject: testNSXProject, + VPCConnectivityProfile: testVPCConnProfile, + PrivateCIDRs: []string{testCIDR1}, + }, + }, + }, + }, + }, + ActiveSystemProvider: netv1alpha1.NetworkProviderVPC, + }, + } +} + +func TestWorkloadNetworkConfiguration_ValidDefaultVPCConfig_Admitted(t *testing.T) { + wnc := vpcWNC() + wnc.Spec.Providers[0].DefaultNamespaceConfiguration = netv1alpha1.NetworkProviderDefaultConfig{ + VPCConfig: &netv1alpha1.DefaultVPCConfig{ + PrivateCIDRs: []string{"10.1.0.0/24"}, + }, + } + if err := k8sClient.Create(testCtx, wnc); err != nil { + t.Fatalf("expected admission, got: %v", err) + } + defer func() { _ = k8sClient.Delete(testCtx, wnc) }() +} + +func TestWorkloadNetworkConfiguration_DefaultVPCConfigOnNonVPCType_Rejected(t *testing.T) { + wnc := vdsWNC() + wnc.Spec.Providers[0].DefaultNamespaceConfiguration = netv1alpha1.NetworkProviderDefaultConfig{ + VPCConfig: &netv1alpha1.DefaultVPCConfig{ + PrivateCIDRs: []string{"10.1.0.0/24"}, + }, + } + err := k8sClient.Create(testCtx, wnc) + if err == nil || !strings.Contains(err.Error(), "defaultNamespaceConfiguration.vpcConfig may only be set when type is vpc") { + t.Fatalf("expected rejection for defaultNamespaceConfiguration.vpcConfig on non-vpc type, got: %v", err) + } +} + +func TestWorkloadNetworkConfiguration_EmptyDefaultNamespaceConfiguration_Rejected(t *testing.T) { + obj := unstrWNC(wncDefaultName, map[string]interface{}{ + "activeSystemProvider": string(netv1alpha1.NetworkProviderVPC), + "providers": []interface{}{ + map[string]interface{}{ + "type": string(netv1alpha1.NetworkProviderVPC), + "systemConfiguration": map[string]interface{}{ + "vpcConfig": map[string]interface{}{ + "autoCreateConfig": map[string]interface{}{ + "nsxProject": testNSXProject, + "vpcConnectivityProfile": testVPCConnProfile, + }, + }, + }, + "defaultNamespaceConfiguration": map[string]interface{}{}, + }, + }, + }) + if err := k8sClient.Create(testCtx, obj); !isRejected(err) { + t.Fatalf("expected rejection for empty defaultNamespaceConfiguration (MinProperties=1), got: %v", err) + } +} + +// TestWorkloadNetworkConfiguration_EmptyPrivateCIDRsList_Rejected sends the +// request as Unstructured because the typed client's `omitempty` drops an +// empty (but non-nil) []string entirely on marshal, which would silently +// turn this into "field absent" instead of testing the MinItems=1 rule. +func TestWorkloadNetworkConfiguration_EmptyPrivateCIDRsList_Rejected(t *testing.T) { + obj := unstrWNC(wncDefaultName, map[string]interface{}{ + "activeSystemProvider": string(netv1alpha1.NetworkProviderVPC), + "providers": []interface{}{ + map[string]interface{}{ + "type": string(netv1alpha1.NetworkProviderVPC), + "systemConfiguration": map[string]interface{}{ + "vpcConfig": map[string]interface{}{ + "autoCreateConfig": map[string]interface{}{ + "nsxProject": testNSXProject, + "vpcConnectivityProfile": testVPCConnProfile, + }, + }, + }, + "defaultNamespaceConfiguration": map[string]interface{}{ + "vpcConfig": map[string]interface{}{ + "privateCIDRs": []interface{}{}, + }, + }, + }, + }, + }) + if err := k8sClient.Create(testCtx, obj); !isRejected(err) { + t.Fatalf("expected rejection for explicit empty privateCIDRs list (MinItems=1), got: %v", err) + } +} + +func TestWorkloadNetworkConfiguration_DefaultPrivateCIDRsFullReplace_Admitted(t *testing.T) { + wnc := vpcWNC() + wnc.Spec.Providers[0].DefaultNamespaceConfiguration = netv1alpha1.NetworkProviderDefaultConfig{ + VPCConfig: &netv1alpha1.DefaultVPCConfig{ + PrivateCIDRs: []string{"10.1.0.0/24"}, + }, + } + if err := k8sClient.Create(testCtx, wnc); err != nil { + t.Fatalf("create: %v", err) + } + defer func() { _ = k8sClient.Delete(testCtx, wnc) }() + + latest := &netv1alpha1.WorkloadNetworkConfiguration{} + if err := k8sClient.Get(testCtx, client.ObjectKeyFromObject(wnc), latest); err != nil { + t.Fatalf("get: %v", err) + } + // Full replace with a CIDR that was never in the original list. Unlike + // systemConfiguration.vpcConfig.autoCreateConfig.privateCIDRs (append-only), + // defaultNamespaceConfiguration.vpcConfig.privateCIDRs has no append-only + // constraint, so dropping 10.1.0.0/24 entirely must be admitted. + latest.Spec.Providers[0].DefaultNamespaceConfiguration.VPCConfig.PrivateCIDRs = []string{"10.2.0.0/24"} + if err := k8sClient.Update(testCtx, latest); err != nil { + t.Fatalf("expected admission for full-replace of defaultNamespaceConfiguration privateCIDRs, got: %v", err) + } +} From bfe1bcb84ca6e0f39941c1cee85df511903bb04f Mon Sep 17 00:00:00 2001 From: Ian Gann Date: Thu, 9 Jul 2026 14:18:14 -0700 Subject: [PATCH 2/2] Code re-org and minor documentation changes --- .../namespacenetworkconfiguration_types.go | 35 -------------- .../workloadnetworkconfiguration_types.go | 47 ++++++++++++++++--- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/api/v1alpha1/namespacenetworkconfiguration_types.go b/api/v1alpha1/namespacenetworkconfiguration_types.go index 468a164..fe18001 100644 --- a/api/v1alpha1/namespacenetworkconfiguration_types.go +++ b/api/v1alpha1/namespacenetworkconfiguration_types.go @@ -197,41 +197,6 @@ type AutoCreateVPCConfig struct { PrivateCIDRs []string `json:"privateCIDRs,omitempty"` } -// NetworkProviderDefaultConfig holds the subset of provider configuration that -// may be independently overridden for new namespaces, distinct from and -// unconstrained by the append-only/immutable rules that apply to -// systemConfiguration. Currently only the vpc provider has such a divergence. -// -// +kubebuilder:validation:MinProperties=1 -type NetworkProviderDefaultConfig struct { - // vpcConfig overrides default values used when auto-creating VPCs for new - // namespaces under the vpc provider. - // - // +optional - VPCConfig *DefaultVPCConfig `json:"vpcConfig,omitempty"` -} - -// DefaultVPCConfig holds VPC default values that may be changed independently -// of the vpc provider's systemConfiguration. -type DefaultVPCConfig struct { - // privateCIDRs replaces the CIDR blocks used as private-subnet/private-pod-IP - // defaults for VPCs auto-created for namespaces onboarding after this is - // set. Already-created namespace VPCs are unaffected. - // - // This value fully replaces (not appends to) the previous default and is - // not subject to the append-only constraint that applies to - // systemConfiguration.vpcConfig.autoCreateConfig.privateCIDRs. When unset, - // new namespaces use systemConfiguration's privateCIDRs instead. - // - // +optional - // +kubebuilder:validation:MinItems=1 - // +kubebuilder:validation:MaxItems=16 - // +kubebuilder:validation:items:MaxLength=64 - // +kubebuilder:validation:items:Pattern=`^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$` - // +listType=atomic - PrivateCIDRs []string `json:"privateCIDRs,omitempty"` -} - // VPCConfig specifies the VPC network configuration for a namespace. // // There are two mutually exclusive modes: diff --git a/api/v1alpha1/workloadnetworkconfiguration_types.go b/api/v1alpha1/workloadnetworkconfiguration_types.go index 2d9b79b..a55a7bf 100644 --- a/api/v1alpha1/workloadnetworkconfiguration_types.go +++ b/api/v1alpha1/workloadnetworkconfiguration_types.go @@ -54,18 +54,53 @@ type NetworkProviderEntry struct { // defaultNamespaceConfiguration optionally overrides configuration values // applied when provisioning new namespaces under this provider, going - // forward. When unset, or when a given sub-field is unset, the equivalent - // value from systemConfiguration is used instead. + // forward. It does not alter the configuration of namespaces already + // associated under this provider. When unset, or when a given sub-field is + // unset, the equivalent value from systemConfiguration is used instead. // - // Unlike systemConfiguration, values here are not tied to the currently - // active system NNC: they may be replaced freely and only take effect for - // namespaces onboarded after the change. Namespaces already associated - // under this provider are unaffected. + // Values here follow different mutability rules than systemConfiguration: + // they may be replaced freely and take effect only for namespaces onboarded + // after the change. // // +optional DefaultNamespaceConfiguration NetworkProviderDefaultConfig `json:"defaultNamespaceConfiguration,omitempty,omitzero"` } +// NetworkProviderDefaultConfig holds the subset of provider configuration that +// may be independently overridden for new namespaces, distinct from and +// unconstrained by the append-only/immutable rules that apply to +// systemConfiguration. +// +// +kubebuilder:validation:MinProperties=1 +type NetworkProviderDefaultConfig struct { + // vpcConfig overrides default values used when auto-creating VPCs for new + // namespaces under the vpc provider. + // + // +optional + VPCConfig *DefaultVPCConfig `json:"vpcConfig,omitempty"` +} + +// DefaultVPCConfig holds VPC default values that may be changed independently +// of the vpc provider's systemConfiguration. +type DefaultVPCConfig struct { + // privateCIDRs sets the CIDR blocks used as private-subnet/private-pod-IP + // defaults for VPCs auto-created for namespaces onboarding after this is + // set. Already-created namespace VPCs are unaffected. + // + // This value fully replaces (not appends to) the previous default and is + // not subject to the append-only constraint that applies to + // systemConfiguration.vpcConfig.autoCreateConfig.privateCIDRs. When unset, + // new namespaces use systemConfiguration's privateCIDRs instead. + // + // +optional + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + // +kubebuilder:validation:items:MaxLength=64 + // +kubebuilder:validation:items:Pattern=`^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$` + // +listType=atomic + PrivateCIDRs []string `json:"privateCIDRs,omitempty"` +} + // +kubebuilder:validation:XValidation:rule="self.providers.exists(p, p.type == self.activeSystemProvider)",message="activeSystemProvider must reference a provider type declared in providers" // WorkloadNetworkConfigurationSpec defines the desired state of the WorkloadNetworkConfiguration.