diff --git a/bindata/network/multus-admission-controller/003-webhook.yaml b/bindata/network/multus-admission-controller/003-webhook.yaml index 11f29bd6fe..e06f098838 100644 --- a/bindata/network/multus-admission-controller/003-webhook.yaml +++ b/bindata/network/multus-admission-controller/003-webhook.yaml @@ -31,6 +31,11 @@ webhooks: # On updates, only validate if the Spec changes - name: CreateDeleteOrUpdatedSpec expression: oldObject == null || object == null || has(object.spec) != has(oldObject.spec) || (has(object.spec) && object.spec != oldObject.spec) +{{if .OVN_PRE_CONF_UDN_ADDR_ENABLE}} + # Ignore default/openshift-ovn-kubernetes NAD to avoid a race between ovn-kubernetes and the multus webhook on install + - name: IgnoreDefaultOVNKubernetesNAD + expression: object == null || object.metadata.namespace != "openshift-ovn-kubernetes" || object.metadata.name != "default" +{{- end }} sideEffects: NoneOnDryRun admissionReviewVersions: - v1 diff --git a/bindata/network/ovn-kubernetes/common/001-crd.yaml b/bindata/network/ovn-kubernetes/common/001-crd.yaml index 3bc0ea6741..d609c793cc 100644 --- a/bindata/network/ovn-kubernetes/common/001-crd.yaml +++ b/bindata/network/ovn-kubernetes/common/001-crd.yaml @@ -3283,6 +3283,50 @@ spec: layer2: description: Layer2 is the Layer2 topology configuration. properties: +{{- if .OVN_PRE_CONF_UDN_ADDR_ENABLE }} + defaultGatewayIPs: + description: |- + defaultGatewayIPs specifies the default gateway IP used in the internal OVN topology. + + Dual-stack clusters may set 2 IPs (one for each IP family), otherwise only 1 IP is allowed. + This field is only allowed for "Primary" network. + It is not recommended to set this field without explicit need and understanding of the OVN network topology. + When omitted, an IP from the subnets field is used. + items: + type: string + x-kubernetes-validations: + - message: IP is invalid + rule: isIP(self) + maxItems: 2 + minItems: 1 + type: array + x-kubernetes-validations: + - message: When 2 IPs are set, they must be from different IP + families + rule: size(self) != 2 || !isIP(self[0]) || !isIP(self[1]) || + ip(self[0]).family() != ip(self[1]).family() + infrastructureSubnets: + description: |- + infrastructureSubnets specifies a list of internal CIDR ranges that OVN-Kubernetes will reserve for internal network infrastructure. + Any IP addresses within these ranges cannot be assigned to workloads. + When omitted, OVN-Kubernetes will automatically allocate IP addresses from `subnets` for its infrastructure needs. + When there are not enough available IPs in the provided infrastructureSubnets, OVN-Kubernetes will automatically allocate IP addresses from subnets for its infrastructure needs. + When `reservedSubnets` is also specified the CIDRs cannot overlap. + When `defaultGatewayIPs` is also specified, the default gateway IPs must belong to one of the infrastructure subnet CIDRs. + Each item should be in range of the specified CIDR(s) in `subnets`. + The maximum number of entries allowed is 4. + The format should match standard CIDR notation (for example, "10.128.0.0/16"). + This field must be omitted if `subnets` is unset or `ipam.mode` is `Disabled`. + items: + maxLength: 43 + type: string + x-kubernetes-validations: + - message: CIDR is invalid + rule: isCIDR(self) + maxItems: 4 + minItems: 1 + type: array +{{- end }} ipam: description: IPAM section contains IPAM-related configuration for the network. @@ -3349,6 +3393,26 @@ spec: maximum: 65536 minimum: 576 type: integer +{{- if .OVN_PRE_CONF_UDN_ADDR_ENABLE }} + reservedSubnets: + description: |- + reservedSubnets specifies a list of CIDRs reserved for static IP assignment, excluded from automatic allocation. + reservedSubnets is optional. When omitted, all IP addresses in `subnets` are available for automatic assignment. + IPs from these ranges can still be requested through static IP assignment. + Each item should be in range of the specified CIDR(s) in `subnets`. + The maximum number of entries allowed is 25. + The format should match standard CIDR notation (for example, "10.128.0.0/16"). + This field must be omitted if `subnets` is unset or `ipam.mode` is `Disabled`. + items: + maxLength: 43 + type: string + x-kubernetes-validations: + - message: CIDR is invalid + rule: isCIDR(self) + maxItems: 25 + minItems: 1 + type: array +{{- end }} role: description: |- Role describes the network role in the pod. @@ -3400,6 +3464,46 @@ spec: is used rule: '!has(self.subnets) || !has(self.mtu) || !self.subnets.exists_one(i, isCIDR(i) && cidr(i).ip().family() == 6) || self.mtu >= 1280' +{{- if .OVN_PRE_CONF_UDN_ADDR_ENABLE }} + - message: defaultGatewayIPs is only supported for Primary network + rule: '!has(self.defaultGatewayIPs) || has(self.role) && self.role + == ''Primary''' + - message: defaultGatewayIPs must belong to one of the subnets specified + in the subnets field + rule: '!has(self.defaultGatewayIPs) || self.defaultGatewayIPs.all(ip, + self.subnets.exists(subnet, cidr(subnet).containsIP(ip)))' + - message: defaultGatewayIPs must be specified for all IP families + rule: '!has(self.defaultGatewayIPs) || size(self.defaultGatewayIPs) + == size(self.subnets)' + - message: reservedSubnets must be unset when subnets is unset + rule: '!has(self.reservedSubnets) || has(self.subnets)' + - message: reservedSubnets is only supported for Primary network + rule: '!has(self.reservedSubnets) || has(self.role) && self.role + == ''Primary''' + - message: infrastructureSubnets must be unset when subnets is unset + rule: '!has(self.infrastructureSubnets) || has(self.subnets)' + - message: infrastructureSubnets is only supported for Primary network + rule: '!has(self.infrastructureSubnets) || has(self.role) && self.role + == ''Primary''' + - message: defaultGatewayIPs have to belong to infrastructureSubnets + rule: '!has(self.infrastructureSubnets) || !has(self.defaultGatewayIPs) + || self.defaultGatewayIPs.all(ip, self.infrastructureSubnets.exists(subnet, + cidr(subnet).containsIP(ip)))' + - fieldPath: .reservedSubnets + message: reservedSubnets must be subnetworks of the networks specified + in the subnets field + rule: '!has(self.reservedSubnets) || self.reservedSubnets.all(e, + self.subnets.exists(s, cidr(s).containsCIDR(cidr(e))))' + - fieldPath: .infrastructureSubnets + message: infrastructureSubnets must be subnetworks of the networks + specified in the subnets field + rule: '!has(self.infrastructureSubnets) || self.infrastructureSubnets.all(e, + self.subnets.exists(s, cidr(s).containsCIDR(cidr(e))))' + - message: infrastructureSubnets and reservedSubnets must not overlap + rule: '!has(self.infrastructureSubnets) || !has(self.reservedSubnets) + || self.infrastructureSubnets.all(infra, !self.reservedSubnets.exists(reserved, + cidr(infra).containsCIDR(reserved) || cidr(reserved).containsCIDR(infra)))' +{{- end }} layer3: description: Layer3 is the Layer3 topology configuration. properties: @@ -3693,6 +3797,50 @@ spec: layer2: description: Layer2 is the Layer2 topology configuration. properties: +{{- if .OVN_PRE_CONF_UDN_ADDR_ENABLE }} + defaultGatewayIPs: + description: |- + defaultGatewayIPs specifies the default gateway IP used in the internal OVN topology. + + Dual-stack clusters may set 2 IPs (one for each IP family), otherwise only 1 IP is allowed. + This field is only allowed for "Primary" network. + It is not recommended to set this field without explicit need and understanding of the OVN network topology. + When omitted, an IP from the subnets field is used. + items: + type: string + x-kubernetes-validations: + - message: IP is invalid + rule: isIP(self) + maxItems: 2 + minItems: 1 + type: array + x-kubernetes-validations: + - message: When 2 IPs are set, they must be from different + IP families + rule: size(self) != 2 || !isIP(self[0]) || !isIP(self[1]) + || ip(self[0]).family() != ip(self[1]).family() + infrastructureSubnets: + description: |- + infrastructureSubnets specifies a list of internal CIDR ranges that OVN-Kubernetes will reserve for internal network infrastructure. + Any IP addresses within these ranges cannot be assigned to workloads. + When omitted, OVN-Kubernetes will automatically allocate IP addresses from `subnets` for its infrastructure needs. + When there are not enough available IPs in the provided infrastructureSubnets, OVN-Kubernetes will automatically allocate IP addresses from subnets for its infrastructure needs. + When `reservedSubnets` is also specified the CIDRs cannot overlap. + When `defaultGatewayIPs` is also specified, the default gateway IPs must belong to one of the infrastructure subnet CIDRs. + Each item should be in range of the specified CIDR(s) in `subnets`. + The maximum number of entries allowed is 4. + The format should match standard CIDR notation (for example, "10.128.0.0/16"). + This field must be omitted if `subnets` is unset or `ipam.mode` is `Disabled`. + items: + maxLength: 43 + type: string + x-kubernetes-validations: + - message: CIDR is invalid + rule: isCIDR(self) + maxItems: 4 + minItems: 1 + type: array +{{- end }} ipam: description: IPAM section contains IPAM-related configuration for the network. @@ -3759,6 +3907,26 @@ spec: maximum: 65536 minimum: 576 type: integer +{{- if .OVN_PRE_CONF_UDN_ADDR_ENABLE }} + reservedSubnets: + description: |- + reservedSubnets specifies a list of CIDRs reserved for static IP assignment, excluded from automatic allocation. + reservedSubnets is optional. When omitted, all IP addresses in `subnets` are available for automatic assignment. + IPs from these ranges can still be requested through static IP assignment. + Each item should be in range of the specified CIDR(s) in `subnets`. + The maximum number of entries allowed is 25. + The format should match standard CIDR notation (for example, "10.128.0.0/16"). + This field must be omitted if `subnets` is unset or `ipam.mode` is `Disabled`. + items: + maxLength: 43 + type: string + x-kubernetes-validations: + - message: CIDR is invalid + rule: isCIDR(self) + maxItems: 25 + minItems: 1 + type: array +{{- end }} role: description: |- Role describes the network role in the pod. @@ -3811,6 +3979,49 @@ spec: subnet is used rule: '!has(self.subnets) || !has(self.mtu) || !self.subnets.exists_one(i, isCIDR(i) && cidr(i).ip().family() == 6) || self.mtu >= 1280' +{{- if .OVN_PRE_CONF_UDN_ADDR_ENABLE }} + - message: defaultGatewayIPs is only supported for Primary network + rule: '!has(self.defaultGatewayIPs) || has(self.role) && self.role + == ''Primary''' + - message: defaultGatewayIPs must belong to one of the subnets + specified in the subnets field + rule: '!has(self.defaultGatewayIPs) || self.defaultGatewayIPs.all(ip, + self.subnets.exists(subnet, cidr(subnet).containsIP(ip)))' + - message: defaultGatewayIPs must be specified for all IP families + rule: '!has(self.defaultGatewayIPs) || size(self.defaultGatewayIPs) + == size(self.subnets)' + - message: reservedSubnets must be unset when subnets is unset + rule: '!has(self.reservedSubnets) || has(self.subnets)' + - message: reservedSubnets is only supported for Primary network + rule: '!has(self.reservedSubnets) || has(self.role) && self.role + == ''Primary''' + - message: infrastructureSubnets must be unset when subnets is + unset + rule: '!has(self.infrastructureSubnets) || has(self.subnets)' + - message: infrastructureSubnets is only supported for Primary + network + rule: '!has(self.infrastructureSubnets) || has(self.role) && + self.role == ''Primary''' + - message: defaultGatewayIPs have to belong to infrastructureSubnets + rule: '!has(self.infrastructureSubnets) || !has(self.defaultGatewayIPs) + || self.defaultGatewayIPs.all(ip, self.infrastructureSubnets.exists(subnet, + cidr(subnet).containsIP(ip)))' + - fieldPath: .reservedSubnets + message: reservedSubnets must be subnetworks of the networks + specified in the subnets field + rule: '!has(self.reservedSubnets) || self.reservedSubnets.all(e, + self.subnets.exists(s, cidr(s).containsCIDR(cidr(e))))' + - fieldPath: .infrastructureSubnets + message: infrastructureSubnets must be subnetworks of the networks + specified in the subnets field + rule: '!has(self.infrastructureSubnets) || self.infrastructureSubnets.all(e, + self.subnets.exists(s, cidr(s).containsCIDR(cidr(e))))' + - message: infrastructureSubnets and reservedSubnets must not + overlap + rule: '!has(self.infrastructureSubnets) || !has(self.reservedSubnets) + || self.infrastructureSubnets.all(infra, !self.reservedSubnets.exists(reserved, + cidr(infra).containsCIDR(reserved) || cidr(reserved).containsCIDR(infra)))' +{{- end }} layer3: description: Layer3 is the Layer3 topology configuration. properties: diff --git a/bindata/network/ovn-kubernetes/common/default-net-annotation-policy.yaml b/bindata/network/ovn-kubernetes/common/default-net-annotation-policy.yaml new file mode 100644 index 0000000000..9619e83715 --- /dev/null +++ b/bindata/network/ovn-kubernetes/common/default-net-annotation-policy.yaml @@ -0,0 +1,38 @@ +{{if .OVN_PRE_CONF_UDN_ADDR_ENABLE}} +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: default-network-annotation +spec: + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["UPDATE"] + resources: ["pods"] + failurePolicy: Fail + validations: + # Prevent any changes to the default-network annotation after pod creation: + # - If annotation exists in old pod: new pod must have same annotation with identical value + # - If annotation doesn't exist in old pod: new pod must also not have it + - expression: > + !has(object.metadata.annotations) || !has(oldObject.metadata.annotations) || + (('v1.multus-cni.io/default-network' in oldObject.metadata.annotations) + ? ('v1.multus-cni.io/default-network' in object.metadata.annotations) && oldObject.metadata.annotations['v1.multus-cni.io/default-network'] == object.metadata.annotations['v1.multus-cni.io/default-network'] + : !('v1.multus-cni.io/default-network' in object.metadata.annotations)) + message: "The 'v1.multus-cni.io/default-network' annotation cannot be changed after the pod was created" +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: default-network-annotation-binding +spec: + policyName: default-network-annotation + validationActions: [Deny] + matchResources: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["UPDATE"] + resources: ["pods"] +{{end}} diff --git a/pkg/network/multus_admission_controller.go b/pkg/network/multus_admission_controller.go index b539664030..0ee3e26ee3 100644 --- a/pkg/network/multus_admission_controller.go +++ b/pkg/network/multus_admission_controller.go @@ -21,6 +21,8 @@ import ( "github.com/openshift/cluster-network-operator/pkg/render" "github.com/pkg/errors" + apifeatures "github.com/openshift/api/features" + "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" uns "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/klog/v2" @@ -54,7 +56,7 @@ func getOpenshiftNamespaces(client cnoclient.Client) (string, error) { } // renderMultusAdmissonControllerConfig returns the manifests of Multus Admisson Controller -func renderMultusAdmissonControllerConfig(manifestDir string, externalControlPlane bool, bootstrapResult *bootstrap.BootstrapResult, client cnoclient.Client, hsc *hypershift.HyperShiftConfig, clientName string) ([]*uns.Unstructured, error) { +func renderMultusAdmissonControllerConfig(manifestDir string, externalControlPlane bool, bootstrapResult *bootstrap.BootstrapResult, client cnoclient.Client, hsc *hypershift.HyperShiftConfig, clientName string, featureGates featuregates.FeatureGate) ([]*uns.Unstructured, error) { objs := []*uns.Unstructured{} var err error @@ -83,6 +85,8 @@ func renderMultusAdmissonControllerConfig(manifestDir string, externalControlPla data.Data["ResourceRequestCPU"] = nil data.Data["ResourceRequestMemory"] = nil data.Data["PriorityClass"] = nil + data.Data["OVN_PRE_CONF_UDN_ADDR_ENABLE"] = featureGates.Enabled(apifeatures.FeatureGatePreconfiguredUDNAddresses) + if hsc.Enabled { data.Data["AdmissionControllerNamespace"] = hsc.Namespace data.Data["KubernetesServiceHost"] = bootstrapResult.Infra.APIServers[bootstrap.APIServerDefaultLocal].Host diff --git a/pkg/network/multus_admission_controller_test.go b/pkg/network/multus_admission_controller_test.go index 3acfa3fd87..7ccda3a603 100644 --- a/pkg/network/multus_admission_controller_test.go +++ b/pkg/network/multus_admission_controller_test.go @@ -62,14 +62,14 @@ func TestRenderMultusAdmissionController(t *testing.T) { bootstrap := fakeBootstrapResult() // disable MultusAdmissionController - objs, err := renderMultusAdmissionController(config, manifestDir, false, bootstrap, fakeClient) + objs, err := renderMultusAdmissionController(config, manifestDir, false, bootstrap, fakeClient, getDefaultFeatureGates()) g.Expect(err).NotTo(HaveOccurred()) g.Expect(objs).NotTo(ContainElement(HaveKubernetesID("Deployment", "openshift-multus", "multus-admission-controller"))) // enable MultusAdmissionController enabled := false config.DisableMultiNetwork = &enabled - objs, err = renderMultusAdmissionController(config, manifestDir, false, bootstrap, fakeClient) + objs, err = renderMultusAdmissionController(config, manifestDir, false, bootstrap, fakeClient, getDefaultFeatureGates()) g.Expect(err).NotTo(HaveOccurred()) g.Expect(objs).To(ContainElement(HaveKubernetesID("Deployment", "openshift-multus", "multus-admission-controller"))) @@ -143,7 +143,7 @@ func TestRenderMultusAdmissonControllerConfigForHyperShift(t *testing.T) { hsc.ReleaseImage = "MyImage" hsc.ControlPlaneImage = "MyCPOImage" - objs, err := renderMultusAdmissonControllerConfig(manifestDir, false, bootstrap, fakeClient, hsc, "") + objs, err := renderMultusAdmissonControllerConfig(manifestDir, false, bootstrap, fakeClient, hsc, "", getDefaultFeatureGates()) g.Expect(err).NotTo(HaveOccurred()) // Check rendered object diff --git a/pkg/network/ovn_kubernetes_test.go b/pkg/network/ovn_kubernetes_test.go index 8287ba8aeb..29f3ef1739 100644 --- a/pkg/network/ovn_kubernetes_test.go +++ b/pkg/network/ovn_kubernetes_test.go @@ -4191,7 +4191,7 @@ func Test_renderOVNKubernetes(t *testing.T) { client: cnofake.NewFakeClient(), featureGates: preDefUDNFeatureGates, }, - expectNumObjs: 45, + expectNumObjs: 47, }, } for _, tt := range tests { diff --git a/pkg/network/render.go b/pkg/network/render.go index ba86e49ca3..38d1c6a516 100644 --- a/pkg/network/render.go +++ b/pkg/network/render.go @@ -65,7 +65,7 @@ func Render(operConf *operv1.NetworkSpec, clusterConf *configv1.NetworkSpec, man // render MultusAdmissionController o, err = renderMultusAdmissionController(operConf, manifestDir, - bootstrapResult.Infra.ControlPlaneTopology == configv1.ExternalTopologyMode, bootstrapResult, client) + bootstrapResult.Infra.ControlPlaneTopology == configv1.ExternalTopologyMode, bootstrapResult, client, featureGates) if err != nil { return nil, progressing, err } @@ -805,7 +805,7 @@ func getMultusAdmissionControllerReplicas(bootstrapResult *bootstrap.BootstrapRe } // renderMultusAdmissionController generates the manifests of Multus Admission Controller -func renderMultusAdmissionController(conf *operv1.NetworkSpec, manifestDir string, externalControlPlane bool, bootstrapResult *bootstrap.BootstrapResult, client cnoclient.Client) ([]*uns.Unstructured, error) { +func renderMultusAdmissionController(conf *operv1.NetworkSpec, manifestDir string, externalControlPlane bool, bootstrapResult *bootstrap.BootstrapResult, client cnoclient.Client, featureGates featuregates.FeatureGate) ([]*uns.Unstructured, error) { if *conf.DisableMultiNetwork { return nil, nil } @@ -815,7 +815,7 @@ func renderMultusAdmissionController(conf *operv1.NetworkSpec, manifestDir strin hsc := hypershift.NewHyperShiftConfig() objs, err := renderMultusAdmissonControllerConfig(manifestDir, externalControlPlane, - bootstrapResult, client, hsc, names.ManagementClusterName) + bootstrapResult, client, hsc, names.ManagementClusterName, featureGates) if err != nil { return nil, err } diff --git a/pkg/network/render_test.go b/pkg/network/render_test.go index 710e487edd..10f7bf41a6 100644 --- a/pkg/network/render_test.go +++ b/pkg/network/render_test.go @@ -8,7 +8,6 @@ import ( . "github.com/onsi/gomega" "github.com/openshift/cluster-network-operator/pkg/client/fake" "github.com/openshift/cluster-network-operator/pkg/hypershift" - "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" "github.com/stretchr/testify/assert" "k8s.io/client-go/kubernetes/scheme" @@ -401,9 +400,7 @@ func TestRenderUnknownNetwork(t *testing.T) { bootstrapResult, err := Bootstrap(&config, client) g.Expect(err).NotTo(HaveOccurred()) - featureGatesCNO := featuregates.NewFeatureGate([]configv1.FeatureGateName{}, []configv1.FeatureGateName{}) - - objs, _, err := Render(prev, &configv1.NetworkSpec{}, manifestDir, client, featureGatesCNO, bootstrapResult) + objs, _, err := Render(prev, &configv1.NetworkSpec{}, manifestDir, client, getDefaultFeatureGates(), bootstrapResult) g.Expect(err).NotTo(HaveOccurred()) // Validate that openshift-sdn isn't rendered