diff --git a/deploy/cluster-manager/chart/cluster-manager/templates/cluster_role.yaml b/deploy/cluster-manager/chart/cluster-manager/templates/cluster_role.yaml index 0bbaecf79..05bc1e0a2 100644 --- a/deploy/cluster-manager/chart/cluster-manager/templates/cluster_role.yaml +++ b/deploy/cluster-manager/chart/cluster-manager/templates/cluster_role.yaml @@ -81,6 +81,10 @@ rules: - apiGroups: ["admissionregistration.k8s.io"] resources: ["validatingwebhookconfigurations", "mutatingwebhookconfigurations"] verbs: ["create", "get", "list", "update", "watch", "patch", "delete"] +# Allow the registration-operator to manage NetworkPolicies +- apiGroups: ["networking.k8s.io"] + resources: ["networkpolicies"] + verbs: ["create", "get", "list", "update", "watch", "patch", "delete"] # Allow the nuclues to manage clustermanager apis. - apiGroups: ["operator.open-cluster-management.io"] resources: ["clustermanagers"] diff --git a/deploy/cluster-manager/config/rbac/cluster_role.yaml b/deploy/cluster-manager/config/rbac/cluster_role.yaml index 5932af19b..218dcbf22 100644 --- a/deploy/cluster-manager/config/rbac/cluster_role.yaml +++ b/deploy/cluster-manager/config/rbac/cluster_role.yaml @@ -83,6 +83,10 @@ rules: - apiGroups: ["admissionregistration.k8s.io"] resources: ["validatingwebhookconfigurations", "mutatingwebhookconfigurations"] verbs: ["create", "get", "list", "update", "watch", "patch", "delete"] +# Allow the registration-operator to manage NetworkPolicies +- apiGroups: ["networking.k8s.io"] + resources: ["networkpolicies"] + verbs: ["create", "get", "list", "update", "watch", "patch", "delete"] # Allow the nuclues to manage clustermanager apis. - apiGroups: ["operator.open-cluster-management.io"] resources: ["clustermanagers"] diff --git a/deploy/cluster-manager/olm-catalog/latest/manifests/cluster-manager.clusterserviceversion.yaml b/deploy/cluster-manager/olm-catalog/latest/manifests/cluster-manager.clusterserviceversion.yaml index 540d8d017..8b185172c 100644 --- a/deploy/cluster-manager/olm-catalog/latest/manifests/cluster-manager.clusterserviceversion.yaml +++ b/deploy/cluster-manager/olm-catalog/latest/manifests/cluster-manager.clusterserviceversion.yaml @@ -59,7 +59,7 @@ metadata: categories: Integration & Delivery,OpenShift Optional certified: "false" containerImage: quay.io/open-cluster-management/registration-operator:latest - createdAt: "2026-06-09T16:57:09Z" + createdAt: "2026-07-29T16:39:49Z" description: Manages the installation and upgrade of the ClusterManager. operators.operatorframework.io/builder: operator-sdk-v1.32.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 @@ -309,6 +309,18 @@ spec: - watch - patch - delete + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - create + - get + - list + - update + - watch + - patch + - delete - apiGroups: - operator.open-cluster-management.io resources: diff --git a/deploy/klusterlet/olm-catalog/latest/manifests/klusterlet.clusterserviceversion.yaml b/deploy/klusterlet/olm-catalog/latest/manifests/klusterlet.clusterserviceversion.yaml index 64693302f..04c2e6bc1 100644 --- a/deploy/klusterlet/olm-catalog/latest/manifests/klusterlet.clusterserviceversion.yaml +++ b/deploy/klusterlet/olm-catalog/latest/manifests/klusterlet.clusterserviceversion.yaml @@ -31,7 +31,7 @@ metadata: categories: Integration & Delivery,OpenShift Optional certified: "false" containerImage: quay.io/open-cluster-management/registration-operator:latest - createdAt: "2026-06-09T16:57:09Z" + createdAt: "2026-07-29T16:39:49Z" description: Manages the installation and upgrade of the Klusterlet. operators.operatorframework.io/builder: operator-sdk-v1.32.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 diff --git a/manifests/cluster-manager/hub/networkpolicies/01-hub-ns-default-deny.yaml b/manifests/cluster-manager/hub/networkpolicies/01-hub-ns-default-deny.yaml new file mode 100644 index 000000000..109c2643d --- /dev/null +++ b/manifests/cluster-manager/hub/networkpolicies/01-hub-ns-default-deny.yaml @@ -0,0 +1,22 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-all + namespace: {{ .ClusterManagerNamespace }} +spec: + podSelector: + matchExpressions: + - key: app + operator: In + values: + - clustermanager-registration-controller + - "{{ .ClusterManagerName }}-registration-webhook" + - "{{ .ClusterManagerName }}-work-controller" + - "{{ .ClusterManagerName }}-work-webhook" + - clustermanager-placement-controller + - clustermanager-addon-manager-controller + - "{{ .ClusterManagerName }}-addon-webhook" + - "{{ .ClusterManagerName }}-grpc-server" + policyTypes: + - Ingress + - Egress diff --git a/manifests/cluster-manager/hub/networkpolicies/02-hub-ns-egress.yaml b/manifests/cluster-manager/hub/networkpolicies/02-hub-ns-egress.yaml new file mode 100644 index 000000000..9f1bca50b --- /dev/null +++ b/manifests/cluster-manager/hub/networkpolicies/02-hub-ns-egress.yaml @@ -0,0 +1,45 @@ +# Combined egress policy for all hub controller and webhook pods. +# Rule 1: DNS resolution (CoreDNS port 53, node-local-dns/openshift-dns port 5353) +# Rule 2: kube-apiserver egress (ports 443, 6443) +# +# No "to" selector is specified on either rule — egress is restricted by port only. +# DNS and apiserver endpoints vary by platform (kube-system vs openshift-dns, +# ClusterIP vs static pod vs external control plane), and locking "to" would +# require platform-specific namespace or IP configuration that OCM intentionally +# avoids. The port restriction limits the blast radius to DNS and HTTPS traffic. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-egress + namespace: {{ .ClusterManagerNamespace }} +spec: + podSelector: + matchExpressions: + - key: app + operator: In + values: + - clustermanager-registration-controller + - "{{ .ClusterManagerName }}-registration-webhook" + - "{{ .ClusterManagerName }}-work-controller" + - "{{ .ClusterManagerName }}-work-webhook" + - clustermanager-placement-controller + - clustermanager-addon-manager-controller + - "{{ .ClusterManagerName }}-addon-webhook" + - "{{ .ClusterManagerName }}-grpc-server" + policyTypes: + - Egress + egress: + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + - port: 5353 + protocol: UDP + - port: 5353 + protocol: TCP + - ports: + - port: 443 + protocol: TCP + - port: 6443 + protocol: TCP diff --git a/manifests/cluster-manager/hub/networkpolicies/03-hub-ns-intra-namespace.yaml b/manifests/cluster-manager/hub/networkpolicies/03-hub-ns-intra-namespace.yaml new file mode 100644 index 000000000..8e36f4245 --- /dev/null +++ b/manifests/cluster-manager/hub/networkpolicies/03-hub-ns-intra-namespace.yaml @@ -0,0 +1,32 @@ +# Intra-namespace pod-to-pod communication. +# Allows pods within open-cluster-management-hub to reach each other +# (e.g. placement-controller debug server, inter-controller calls). +# API-server webhook ingress is handled separately by 04-hub-ns-webhook-ingress.yaml. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-intra-namespace + namespace: {{ .ClusterManagerNamespace }} +spec: + podSelector: &ocmPods + matchExpressions: + - key: app + operator: In + values: + - clustermanager-registration-controller + - "{{ .ClusterManagerName }}-registration-webhook" + - "{{ .ClusterManagerName }}-work-controller" + - "{{ .ClusterManagerName }}-work-webhook" + - clustermanager-placement-controller + - clustermanager-addon-manager-controller + - "{{ .ClusterManagerName }}-addon-webhook" + - "{{ .ClusterManagerName }}-grpc-server" + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: *ocmPods + egress: + - to: + - podSelector: *ocmPods diff --git a/manifests/cluster-manager/hub/networkpolicies/04-hub-ns-webhook-ingress.yaml b/manifests/cluster-manager/hub/networkpolicies/04-hub-ns-webhook-ingress.yaml new file mode 100644 index 000000000..94826109a --- /dev/null +++ b/manifests/cluster-manager/hub/networkpolicies/04-hub-ns-webhook-ingress.yaml @@ -0,0 +1,42 @@ +# Ingress policy for all externally-reachable hub pods. +# Always applied when NetworkPolicies feature gate is enabled — restricted by pod label and port only. +# Intentionally allows ingress from all sources: webhook sources vary by platform (kube-system, +# openshift-kube-apiserver, external control planes), and OCM cannot assume a platform-specific +# source namespace. Port restriction limits the blast radius to webhook, gRPC, and debug ports. +# +# Covers: +# - registration/work/addon webhooks (apiserver admission calls) +# - grpc-server (spoke cluster connections, ports 8090/443) +# - placement debug server (port 9443) +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-hub-ingress + namespace: {{ .ClusterManagerNamespace }} +spec: + podSelector: + matchExpressions: + - key: app + operator: In + values: + - "{{ .ClusterManagerName }}-registration-webhook" + - "{{ .ClusterManagerName }}-work-webhook" + - "{{ .ClusterManagerName }}-addon-webhook" + - "{{ .ClusterManagerName }}-grpc-server" + - clustermanager-placement-controller + policyTypes: + - Ingress + ingress: + - ports: + - port: {{ .RegistrationWebhook.Port }} + protocol: TCP + - port: {{ .WorkWebhook.Port }} + protocol: TCP + - port: {{ .AddonWebhook.Port }} + protocol: TCP + - port: 8090 + protocol: TCP + - port: 443 + protocol: TCP + - port: 9443 + protocol: TCP diff --git a/manifests/cluster-manager/hub/networkpolicies/05-hub-ns-prometheus.yaml b/manifests/cluster-manager/hub/networkpolicies/05-hub-ns-prometheus.yaml new file mode 100644 index 000000000..df0b7087a --- /dev/null +++ b/manifests/cluster-manager/hub/networkpolicies/05-hub-ns-prometheus.yaml @@ -0,0 +1,29 @@ +# Prometheus scrape ingress for all hub controller and webhook pods. +# Allows ingress on metrics ports from any source within the cluster. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-prometheus-ingress + namespace: {{ .ClusterManagerNamespace }} +spec: + podSelector: + matchExpressions: + - key: app + operator: In + values: + - clustermanager-registration-controller + - "{{ .ClusterManagerName }}-registration-webhook" + - "{{ .ClusterManagerName }}-work-controller" + - "{{ .ClusterManagerName }}-work-webhook" + - clustermanager-placement-controller + - clustermanager-addon-manager-controller + - "{{ .ClusterManagerName }}-addon-webhook" + - "{{ .ClusterManagerName }}-grpc-server" + policyTypes: + - Ingress + ingress: + - ports: + - port: 8080 + protocol: TCP + - port: 8443 + protocol: TCP diff --git a/manifests/config.go b/manifests/config.go index 0da4df289..f55f80181 100644 --- a/manifests/config.go +++ b/manifests/config.go @@ -55,6 +55,8 @@ type HubConfig struct { // TLS configuration injected into all managed hub component deployments TLSMinVersion string TLSCipherSuites string + + NetworkPoliciesEnabled bool } type Webhook struct { diff --git a/manifests/klusterlet/management/klusterlet-agent-networkpolicy.yaml b/manifests/klusterlet/management/klusterlet-agent-networkpolicy.yaml index e512177d6..15872d2b1 100644 --- a/manifests/klusterlet/management/klusterlet-agent-networkpolicy.yaml +++ b/manifests/klusterlet/management/klusterlet-agent-networkpolicy.yaml @@ -14,18 +14,10 @@ spec: - from: - podSelector: {} egress: - - to: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: default - ports: + - ports: - protocol: TCP port: 443 - - to: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: open-cluster-management-hub - ports: + - ports: - protocol: TCP port: 9443 - to: diff --git a/manifests/klusterlet/management/klusterlet-allow-dns-networkpolicy.yaml b/manifests/klusterlet/management/klusterlet-allow-dns-networkpolicy.yaml index 297a608de..d8e2aad35 100644 --- a/manifests/klusterlet/management/klusterlet-allow-dns-networkpolicy.yaml +++ b/manifests/klusterlet/management/klusterlet-allow-dns-networkpolicy.yaml @@ -8,23 +8,12 @@ spec: policyTypes: - Egress egress: - - to: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: openshift-dns - ports: + - ports: - protocol: UDP port: 5353 - protocol: TCP port: 5353 - - to: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: kube-system - podSelector: - matchLabels: - k8s-app: kube-dns - ports: + - ports: - protocol: UDP port: 53 - protocol: TCP diff --git a/manifests/klusterlet/management/klusterlet-networkpolicy.yaml b/manifests/klusterlet/management/klusterlet-networkpolicy.yaml index a580b0d74..4a4d8a885 100644 --- a/manifests/klusterlet/management/klusterlet-networkpolicy.yaml +++ b/manifests/klusterlet/management/klusterlet-networkpolicy.yaml @@ -16,17 +16,6 @@ spec: egress: - to: - podSelector: {} - - to: - - namespaceSelector: - matchLabels: - addon.open-cluster-management.io/namespace: "true" - ports: - - protocol: TCP - port: 443 - - to: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: default - ports: + - ports: - protocol: TCP port: 443 diff --git a/pkg/operator/helpers/helpers.go b/pkg/operator/helpers/helpers.go index 2eab4fc10..04ccb6011 100644 --- a/pkg/operator/helpers/helpers.go +++ b/pkg/operator/helpers/helpers.go @@ -805,6 +805,28 @@ func FeatureGateEnabled(features []operatorapiv1.FeatureGate, return defaultFeature.Default } +// NetworkPolicies is an operator-internal feature gate that controls whether NetworkPolicy +// manifests are applied to the hub/agent namespace. Not passed through to agent binaries. +const NetworkPolicies featuregate.Feature = "NetworkPolicies" + +// operatorOnlyFeatureGates are feature gates consumed by the operator itself, +// not passed through to agent binaries as CLI flags. +var operatorOnlyFeatureGates = map[featuregate.Feature]bool{ + NetworkPolicies: true, +} + +// FilterOperatorFeatureGates removes operator-internal feature gates from the list +// so they are not forwarded as CLI flags to hub/agent binaries. +func FilterOperatorFeatureGates(features []operatorapiv1.FeatureGate) []operatorapiv1.FeatureGate { + var filtered []operatorapiv1.FeatureGate + for _, f := range features { + if !operatorOnlyFeatureGates[featuregate.Feature(f.Feature)] { + filtered = append(filtered, f) + } + } + return filtered +} + // IsSingleton returns if agent is deployed in singleton mode either hosted or not func IsSingleton(mode operatorapiv1.InstallMode) bool { return mode == operatorapiv1.InstallModeSingleton || mode == operatorapiv1.InstallModeSingletonHosted diff --git a/pkg/operator/helpers/helpers_test.go b/pkg/operator/helpers/helpers_test.go index 38c204999..943fa05c3 100644 --- a/pkg/operator/helpers/helpers_test.go +++ b/pkg/operator/helpers/helpers_test.go @@ -17,6 +17,7 @@ import ( admissionv1 "k8s.io/api/admissionregistration/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" fakeapiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -2442,6 +2443,195 @@ func TestNormalizeImagePullSecretName(t *testing.T) { } } +// networkPolicyPort is a comparable, protocol+port pair used to assert on the +// exact ports of a specific NetworkPolicy rule (avoids ambiguous substring +// matches on the rendered YAML, e.g. "port: 53" matching inside "port: 5353"). +type networkPolicyPort struct { + protocol corev1.Protocol + port int32 +} + +func portsOf(ports []networkingv1.NetworkPolicyPort) []networkPolicyPort { + got := make([]networkPolicyPort, 0, len(ports)) + for _, p := range ports { + var proto corev1.Protocol + if p.Protocol != nil { + proto = *p.Protocol + } + var port int32 + if p.Port != nil { + port = p.Port.IntVal + } + got = append(got, networkPolicyPort{protocol: proto, port: port}) + } + return got +} + +func TestNetworkPolicyTemplateRendering(t *testing.T) { + cases := []struct { + name string + config manifests.HubConfig + manifestFile string + expectContains []string + expectAbsent []string + expectedPolicyTypes []networkingv1.PolicyType + // expectedIngressPorts/expectedEgressPorts, when non-nil, must match the + // ports of each ingress/egress rule, in order, exactly. + expectedIngressPorts [][]networkPolicyPort + expectedEgressPorts [][]networkPolicyPort + }{ + { + name: "default deny scoped to OCM pods", + config: manifests.HubConfig{ + ClusterManagerNamespace: "open-cluster-management-hub", + ClusterManagerName: "cluster-manager", + }, + manifestFile: "cluster-manager/hub/networkpolicies/01-hub-ns-default-deny.yaml", + expectContains: []string{ + "namespace: open-cluster-management-hub", + "clustermanager-registration-controller", + "cluster-manager-work-controller", + "clustermanager-placement-controller", + "clustermanager-addon-manager-controller", + "cluster-manager-grpc-server", + }, + expectedPolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, + }, + { + name: "egress policy scoped to OCM pods", + config: manifests.HubConfig{ + ClusterManagerNamespace: "open-cluster-management-hub", + ClusterManagerName: "cluster-manager", + }, + manifestFile: "cluster-manager/hub/networkpolicies/02-hub-ns-egress.yaml", + expectContains: []string{ + "allow-egress", + "clustermanager-registration-controller", + "cluster-manager-grpc-server", + }, + expectedPolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeEgress}, + // Rule 0: DNS (port 53 distinct from port 5353). Rule 1: kube-apiserver. + expectedEgressPorts: [][]networkPolicyPort{ + { + {protocol: corev1.ProtocolUDP, port: 53}, + {protocol: corev1.ProtocolTCP, port: 53}, + {protocol: corev1.ProtocolUDP, port: 5353}, + {protocol: corev1.ProtocolTCP, port: 5353}, + }, + { + {protocol: corev1.ProtocolTCP, port: 443}, + {protocol: corev1.ProtocolTCP, port: 6443}, + }, + }, + }, + { + name: "hub ingress covers webhooks, grpc-server, and placement debug — all sources intentionally allowed", + config: manifests.HubConfig{ + ClusterManagerNamespace: "open-cluster-management-hub", + ClusterManagerName: "cluster-manager", + RegistrationWebhook: manifests.Webhook{Port: 9443}, + WorkWebhook: manifests.Webhook{Port: 9443}, + AddonWebhook: manifests.Webhook{Port: 9443}, + }, + manifestFile: "cluster-manager/hub/networkpolicies/04-hub-ns-webhook-ingress.yaml", + expectContains: []string{ + "allow-hub-ingress", + "cluster-manager-registration-webhook", + "cluster-manager-work-webhook", + "cluster-manager-addon-webhook", + "cluster-manager-grpc-server", + "clustermanager-placement-controller", + }, + expectAbsent: []string{"namespaceSelector"}, + expectedPolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, + expectedIngressPorts: [][]networkPolicyPort{ + { + {protocol: corev1.ProtocolTCP, port: 9443}, // RegistrationWebhook.Port + {protocol: corev1.ProtocolTCP, port: 9443}, // WorkWebhook.Port + {protocol: corev1.ProtocolTCP, port: 9443}, // AddonWebhook.Port + {protocol: corev1.ProtocolTCP, port: 8090}, // grpc-server + {protocol: corev1.ProtocolTCP, port: 443}, // grpc-server + {protocol: corev1.ProtocolTCP, port: 9443}, // placement debug + }, + }, + }, + { + name: "prometheus NP uses ClusterManagerName vars", + config: manifests.HubConfig{ + ClusterManagerNamespace: "open-cluster-management-hub", + ClusterManagerName: "cluster-manager", + }, + manifestFile: "cluster-manager/hub/networkpolicies/05-hub-ns-prometheus.yaml", + expectContains: []string{ + "clustermanager-registration-controller", + "cluster-manager-work-controller", + "clustermanager-placement-controller", + "cluster-manager-grpc-server", + }, + expectAbsent: []string{"namespaceSelector"}, + expectedPolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, + expectedIngressPorts: [][]networkPolicyPort{ + { + {protocol: corev1.ProtocolTCP, port: 8080}, + {protocol: corev1.ProtocolTCP, port: 8443}, + }, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + template, err := manifests.ClusterManagerManifestFiles.ReadFile(c.manifestFile) + if err != nil { + t.Fatalf("failed to read manifest %s: %v", c.manifestFile, err) + } + rendered := assets.MustCreateAssetFromTemplate(c.manifestFile, template, c.config).Data + + for _, want := range c.expectContains { + if !strings.Contains(string(rendered), want) { + t.Errorf("expected rendered manifest to contain %q\ngot:\n%s", want, rendered) + } + } + for _, absent := range c.expectAbsent { + if strings.Contains(string(rendered), absent) { + t.Errorf("expected rendered manifest NOT to contain %q\ngot:\n%s", absent, rendered) + } + } + + var np networkingv1.NetworkPolicy + if err := yaml.Unmarshal(rendered, &np); err != nil { + t.Fatalf("failed to unmarshal rendered manifest into NetworkPolicy: %v\ngot:\n%s", err, rendered) + } + + if c.expectedPolicyTypes != nil && !reflect.DeepEqual(np.Spec.PolicyTypes, c.expectedPolicyTypes) { + t.Errorf("expected policyTypes %v, got %v", c.expectedPolicyTypes, np.Spec.PolicyTypes) + } + + if c.expectedIngressPorts != nil { + if len(np.Spec.Ingress) != len(c.expectedIngressPorts) { + t.Fatalf("expected %d ingress rule(s), got %d: %+v", len(c.expectedIngressPorts), len(np.Spec.Ingress), np.Spec.Ingress) + } + for i, rule := range np.Spec.Ingress { + if got, want := portsOf(rule.Ports), c.expectedIngressPorts[i]; !reflect.DeepEqual(got, want) { + t.Errorf("ingress rule %d: expected ports %+v, got %+v", i, want, got) + } + } + } + + if c.expectedEgressPorts != nil { + if len(np.Spec.Egress) != len(c.expectedEgressPorts) { + t.Fatalf("expected %d egress rule(s), got %d: %+v", len(c.expectedEgressPorts), len(np.Spec.Egress), np.Spec.Egress) + } + for i, rule := range np.Spec.Egress { + if got, want := portsOf(rule.Ports), c.expectedEgressPorts[i]; !reflect.DeepEqual(got, want) { + t.Errorf("egress rule %d: expected ports %+v, got %+v", i, want, got) + } + } + } + }) + } +} + func TestRegistrationClusterRoleImagePullSecretName(t *testing.T) { template, err := manifests.ClusterManagerManifestFiles.ReadFile("cluster-manager/hub/registration/clusterrole.yaml") if err != nil { diff --git a/pkg/operator/operators/clustermanager/controllers/clustermanagercontroller/clustermanager_controller.go b/pkg/operator/operators/clustermanager/controllers/clustermanagercontroller/clustermanager_controller.go index 2cd895d68..9b8342cb3 100644 --- a/pkg/operator/operators/clustermanager/controllers/clustermanagercontroller/clustermanager_controller.go +++ b/pkg/operator/operators/clustermanager/controllers/clustermanagercontroller/clustermanager_controller.go @@ -195,8 +195,9 @@ func (n *clusterManagerController) sync(ctx context.Context, controllerContext f registrationFeatureGates = clusterManager.Spec.RegistrationConfiguration.FeatureGates config.AutoApproveUsers = strings.Join(clusterManager.Spec.RegistrationConfiguration.AutoApproveUsers, ",") } + config.NetworkPoliciesEnabled = helpers.FeatureGateEnabled(registrationFeatureGates, ocmfeature.DefaultHubRegistrationFeatureGates, helpers.NetworkPolicies) config.RegistrationFeatureGates, registrationFeatureMsgs = helpers.ConvertToFeatureGateFlags("Registration", - registrationFeatureGates, ocmfeature.DefaultHubRegistrationFeatureGates) + helpers.FilterOperatorFeatureGates(registrationFeatureGates), ocmfeature.DefaultHubRegistrationFeatureGates) config.ClusterProfileEnabled = helpers.FeatureGateEnabled(registrationFeatureGates, ocmfeature.DefaultHubRegistrationFeatureGates, ocmfeature.ClusterProfile) // setting for cluster importer. // TODO(qiujian16) since this is disabled by feature gate, the image is obtained from cluster manager's env var. Need a more elegant approach. diff --git a/pkg/operator/operators/clustermanager/controllers/clustermanagercontroller/clustermanager_controller_test.go b/pkg/operator/operators/clustermanager/controllers/clustermanagercontroller/clustermanager_controller_test.go index 4b047f10f..1cca4a8a5 100644 --- a/pkg/operator/operators/clustermanager/controllers/clustermanagercontroller/clustermanager_controller_test.go +++ b/pkg/operator/operators/clustermanager/controllers/clustermanagercontroller/clustermanager_controller_test.go @@ -1709,3 +1709,77 @@ func containsArg(args []string, prefix string) bool { } return false } + +func TestNetworkPolicyFeatureGate(t *testing.T) { + cases := []struct { + name string + featureGates []operatorapiv1.FeatureGate + expectBaseNPs bool + expectPrometheusNP bool + }{ + { + name: "feature gate disabled — no NPs", + featureGates: nil, + expectBaseNPs: false, + expectPrometheusNP: false, + }, + { + name: "feature gate enabled — all NPs applied", + featureGates: []operatorapiv1.FeatureGate{ + {Feature: "NetworkPolicies", Mode: operatorapiv1.FeatureGateModeTypeEnable}, + }, + expectBaseNPs: true, + expectPrometheusNP: true, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + clusterManager := newClusterManager("testhub") + if c.featureGates != nil { + if clusterManager.Spec.RegistrationConfiguration == nil { + clusterManager.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationHubConfiguration{} + } + clusterManager.Spec.RegistrationConfiguration.FeatureGates = c.featureGates + } + + tc := newTestController(t, clusterManager) + clusterManagerNamespace := helpers.ClusterManagerNamespace(clusterManager.Name, clusterManager.Spec.DeployOption.Mode) + setup(t, tc, setDeployment(clusterManager.Name, clusterManagerNamespace)) + + syncContext := testingcommon.NewFakeSyncContext(t, "testhub") + if err := tc.clusterManagerController.sync(ctx, syncContext, "testhub"); err != nil { + t.Fatalf("unexpected sync error: %v", err) + } + + baseNPNames := []string{"default-deny-all", "allow-egress", "allow-intra-namespace", "allow-hub-ingress"} + createdNPs := map[string]bool{} + for _, action := range tc.hubKubeClient.Actions() { + if action.GetVerb() != createVerb { + continue + } + obj := action.(clienttesting.CreateActionImpl).Object + if np, ok := obj.(interface{ GetName() string }); ok { + createdNPs[np.GetName()] = true + } + } + + if c.expectBaseNPs { + for _, name := range baseNPNames { + if !createdNPs[name] { + t.Errorf("expectBaseNPs=true but NetworkPolicy %q was not created", name) + } + } + } else { + for _, name := range baseNPNames { + if createdNPs[name] { + t.Errorf("expectBaseNPs=false but NetworkPolicy %q was created", name) + } + } + } + if hasPrometheus := createdNPs["allow-prometheus-ingress"]; hasPrometheus != c.expectPrometheusNP { + t.Errorf("expectPrometheusNP=%v but hasPrometheusNP=%v", c.expectPrometheusNP, hasPrometheus) + } + }) + } +} diff --git a/pkg/operator/operators/clustermanager/controllers/clustermanagercontroller/clustermanager_hub_reconcile.go b/pkg/operator/operators/clustermanager/controllers/clustermanagercontroller/clustermanager_hub_reconcile.go index 6332c9105..694284691 100644 --- a/pkg/operator/operators/clustermanager/controllers/clustermanagercontroller/clustermanager_hub_reconcile.go +++ b/pkg/operator/operators/clustermanager/controllers/clustermanagercontroller/clustermanager_hub_reconcile.go @@ -87,6 +87,17 @@ var ( "cluster-manager/hub/grpc-server/serviceaccount.yaml", "cluster-manager/hub/grpc-server/service.yaml", } + + // hubNetworkPolicyBaseFiles are always-on NetworkPolicy resources for open-cluster-management-hub. + // Applied as part of ClusterManager CR reconciliation. + hubNetworkPolicyBaseFiles = []string{ + "cluster-manager/hub/networkpolicies/01-hub-ns-default-deny.yaml", + "cluster-manager/hub/networkpolicies/02-hub-ns-egress.yaml", + "cluster-manager/hub/networkpolicies/03-hub-ns-intra-namespace.yaml", + "cluster-manager/hub/networkpolicies/04-hub-ns-webhook-ingress.yaml", + } + + hubPrometheusNPFile = "cluster-manager/hub/networkpolicies/05-hub-ns-prometheus.yaml" ) type hubReconcile struct { @@ -195,5 +206,10 @@ func getHubResources(mode operatorapiv1.InstallMode, config manifests.HubConfig) hubResources = append(hubResources, hubDefaultWebhookServiceFiles...) } + if config.NetworkPoliciesEnabled { + hubResources = append(hubResources, hubNetworkPolicyBaseFiles...) + hubResources = append(hubResources, hubPrometheusNPFile) + } + return hubResources } diff --git a/pkg/operator/operators/klusterlet/controllers/klusterletcontroller/klusterlet_controller.go b/pkg/operator/operators/klusterlet/controllers/klusterletcontroller/klusterlet_controller.go index 856462305..4e19d8fdc 100644 --- a/pkg/operator/operators/klusterlet/controllers/klusterletcontroller/klusterlet_controller.go +++ b/pkg/operator/operators/klusterlet/controllers/klusterletcontroller/klusterlet_controller.go @@ -19,7 +19,6 @@ import ( coreinformer "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" - "k8s.io/component-base/featuregate" "k8s.io/klog/v2" operatorv1client "open-cluster-management.io/api/client/operator/clientset/versioned/typed/operator/v1" @@ -46,19 +45,8 @@ const ( klusterletFinalizer = "operator.open-cluster-management.io/klusterlet-cleanup" managedResourcesEvictionTimestampAnno = "operator.open-cluster-management.io/managed-resources-eviction-timestamp" klusterletNamespaceLabelKey = "operator.open-cluster-management.io/klusterlet" - - // NetworkPolicies is an operator-internal feature gate that controls whether NetworkPolicy - // manifests are applied to the agent namespace. Disabled by default; enable via the Klusterlet - // CR's registrationConfiguration.featureGates. - NetworkPolicies featuregate.Feature = "NetworkPolicies" ) -// operatorOnlyFeatureGates are feature gates consumed by the operator itself, -// not passed through to agent binaries as CLI flags. -var operatorOnlyFeatureGates = map[featuregate.Feature]bool{ - NetworkPolicies: true, -} - // buildClusterAnnotationsString renders the ClusterAnnotations map into the // comma-separated "key1=value1,key2=value2" form consumed by the registration // agent's --cluster-annotations flag. Keys are sorted deterministically. @@ -72,16 +60,6 @@ func buildClusterAnnotationsString(annotations map[string]string) string { return strings.Join(arr, ",") } -func filterOperatorFeatureGates(features []operatorapiv1.FeatureGate) []operatorapiv1.FeatureGate { - var filtered []operatorapiv1.FeatureGate - for _, f := range features { - if !operatorOnlyFeatureGates[featuregate.Feature(f.Feature)] { - filtered = append(filtered, f) - } - } - return filtered -} - type klusterletController struct { patcher patcher.Patcher[*operatorapiv1.Klusterlet, operatorapiv1.KlusterletSpec, operatorapiv1.KlusterletStatus] klusterletLister operatorlister.KlusterletLister @@ -455,9 +433,9 @@ func (n *klusterletController) sync(ctx context.Context, controllerContext facto config.AboutAPIEnabled = helpers.FeatureGateEnabled( registrationFeatureGates, ocmfeature.DefaultSpokeRegistrationFeatureGates, ocmfeature.ClusterProperty) config.NetworkPoliciesEnabled = helpers.FeatureGateEnabled( - registrationFeatureGates, ocmfeature.DefaultSpokeRegistrationFeatureGates, NetworkPolicies) + registrationFeatureGates, ocmfeature.DefaultSpokeRegistrationFeatureGates, helpers.NetworkPolicies) config.RegistrationFeatureGates, registrationFeatureMsgs = helpers.ConvertToFeatureGateFlags("Registration", - filterOperatorFeatureGates(registrationFeatureGates), ocmfeature.DefaultSpokeRegistrationFeatureGates) + helpers.FilterOperatorFeatureGates(registrationFeatureGates), ocmfeature.DefaultSpokeRegistrationFeatureGates) var workFeatureGates []operatorapiv1.FeatureGate if klusterlet.Spec.WorkConfiguration != nil { diff --git a/pkg/operator/operators/klusterlet/controllers/klusterletcontroller/klusterlet_controller_test.go b/pkg/operator/operators/klusterlet/controllers/klusterletcontroller/klusterlet_controller_test.go index f01226766..cdd94122c 100644 --- a/pkg/operator/operators/klusterlet/controllers/klusterletcontroller/klusterlet_controller_test.go +++ b/pkg/operator/operators/klusterlet/controllers/klusterletcontroller/klusterlet_controller_test.go @@ -668,7 +668,7 @@ func TestSyncDeployWithNetworkPolicies(t *testing.T) { klusterlet := newKlusterlet("klusterlet", "testns", "cluster1") klusterlet.Spec.RegistrationConfiguration.FeatureGates = append( klusterlet.Spec.RegistrationConfiguration.FeatureGates, - operatorapiv1.FeatureGate{Feature: string(NetworkPolicies), Mode: operatorapiv1.FeatureGateModeTypeEnable}, + operatorapiv1.FeatureGate{Feature: string(helpers.NetworkPolicies), Mode: operatorapiv1.FeatureGateModeTypeEnable}, ) bootStrapSecret := newSecret(helpers.BootstrapHubKubeConfig, "testns") hubKubeConfigSecret := newSecret(helpers.HubKubeConfig, "testns") diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index ba3018473..2890e2451 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -170,6 +170,11 @@ var _ = BeforeSuite(func() { return hub.EnableHubRegistrationFeature("ClusterImporter") }).Should(Succeed()) + By("Enable NetworkPolicies Feature") + Eventually(func() error { + return hub.EnableHubRegistrationFeature("NetworkPolicies") + }).Should(Succeed()) + By("Enable CleanUpCompletedManifestWork feature gate") Eventually(func() error { return hub.EnableHubWorkFeature("CleanUpCompletedManifestWork") diff --git a/test/integration/operator/clustermanager_aws_test.go b/test/integration/operator/clustermanager_aws_test.go index cb7a4852e..3a069bd6a 100644 --- a/test/integration/operator/clustermanager_aws_test.go +++ b/test/integration/operator/clustermanager_aws_test.go @@ -44,19 +44,19 @@ var _ = ginkgo.Describe("ClusterManager Default Mode with aws registration", fun if clusterManager.Spec.RegistrationConfiguration == nil { clusterManager.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationHubConfiguration{} - clusterManager.Spec.RegistrationConfiguration.RegistrationDrivers = []operatorapiv1.RegistrationDriverHub{ - { - AuthType: operatorapiv1.AwsIrsaAuthType, - AwsIrsa: &operatorapiv1.AwsIrsaConfig{ - HubClusterArn: hubClusterArn, - Tags: []string{ - "product:v1:tenant:app-name=My-App", - "product:v1:tenant:created-by=Team-1", - }, - AutoApprovedIdentities: []string{"arn:aws:eks:us-west-2:123456789013:cluster/.*", "arn:aws:eks:us-west-2:123456789012:cluster/.*"}, + } + clusterManager.Spec.RegistrationConfiguration.RegistrationDrivers = []operatorapiv1.RegistrationDriverHub{ + { + AuthType: operatorapiv1.AwsIrsaAuthType, + AwsIrsa: &operatorapiv1.AwsIrsaConfig{ + HubClusterArn: hubClusterArn, + Tags: []string{ + "product:v1:tenant:app-name=My-App", + "product:v1:tenant:created-by=Team-1", }, + AutoApprovedIdentities: []string{"arn:aws:eks:us-west-2:123456789013:cluster/.*", "arn:aws:eks:us-west-2:123456789012:cluster/.*"}, }, - } + }, } _, err = operatorClient.OperatorV1().ClusterManagers().Update(context.Background(), clusterManager, metav1.UpdateOptions{}) @@ -70,7 +70,17 @@ var _ = ginkgo.Describe("ClusterManager Default Mode with aws registration", fun if err != nil { return err } - clusterManager.Spec.RegistrationConfiguration = nil + var featureGates []operatorapiv1.FeatureGate + if clusterManager.Spec.RegistrationConfiguration != nil { + featureGates = clusterManager.Spec.RegistrationConfiguration.FeatureGates + } + if len(featureGates) == 0 { + clusterManager.Spec.RegistrationConfiguration = nil + } else { + clusterManager.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationHubConfiguration{ + FeatureGates: featureGates, + } + } _, err = operatorClient.OperatorV1().ClusterManagers().Update(context.Background(), clusterManager, metav1.UpdateOptions{}) return err @@ -141,14 +151,14 @@ var _ = ginkgo.Describe("ClusterManager Default Mode with aws registration", fun if clusterManager.Spec.RegistrationConfiguration == nil { clusterManager.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationHubConfiguration{} - clusterManager.Spec.RegistrationConfiguration.RegistrationDrivers = []operatorapiv1.RegistrationDriverHub{ - { - AuthType: operatorapiv1.AwsIrsaAuthType, - AwsIrsa: &operatorapiv1.AwsIrsaConfig{ - HubClusterArn: hubClusterArn, - }, + } + clusterManager.Spec.RegistrationConfiguration.RegistrationDrivers = []operatorapiv1.RegistrationDriverHub{ + { + AuthType: operatorapiv1.AwsIrsaAuthType, + AwsIrsa: &operatorapiv1.AwsIrsaConfig{ + HubClusterArn: hubClusterArn, }, - } + }, } _, err = operatorClient.OperatorV1().ClusterManagers().Update(context.Background(), clusterManager, metav1.UpdateOptions{}) @@ -162,7 +172,17 @@ var _ = ginkgo.Describe("ClusterManager Default Mode with aws registration", fun if err != nil { return err } - clusterManager.Spec.RegistrationConfiguration = nil + var featureGates []operatorapiv1.FeatureGate + if clusterManager.Spec.RegistrationConfiguration != nil { + featureGates = clusterManager.Spec.RegistrationConfiguration.FeatureGates + } + if len(featureGates) == 0 { + clusterManager.Spec.RegistrationConfiguration = nil + } else { + clusterManager.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationHubConfiguration{ + FeatureGates: featureGates, + } + } _, err = operatorClient.OperatorV1().ClusterManagers().Update(context.Background(), clusterManager, metav1.UpdateOptions{}) return err diff --git a/test/integration/operator/clustermanager_grpc_test.go b/test/integration/operator/clustermanager_grpc_test.go index ea67693b6..74b6df8cb 100644 --- a/test/integration/operator/clustermanager_grpc_test.go +++ b/test/integration/operator/clustermanager_grpc_test.go @@ -327,31 +327,37 @@ func enableGRPCAuth(operatorClient operatorclient.Interface, clusterManagerName return err } - clusterManager.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationHubConfiguration{ - RegistrationDrivers: []operatorapiv1.RegistrationDriverHub{ + var featureGates []operatorapiv1.FeatureGate + if clusterManager.Spec.RegistrationConfiguration != nil { + featureGates = clusterManager.Spec.RegistrationConfiguration.FeatureGates + } + + registrationDrivers := []operatorapiv1.RegistrationDriverHub{ + { + AuthType: operatorapiv1.CSRAuthType, + }, + { + AuthType: operatorapiv1.GRPCAuthType, + }, + } + if len(autoApprovedIdentities) != 0 { + registrationDrivers = []operatorapiv1.RegistrationDriverHub{ { AuthType: operatorapiv1.CSRAuthType, }, { AuthType: operatorapiv1.GRPCAuthType, - }, - }, - } - if len(autoApprovedIdentities) != 0 { - clusterManager.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationHubConfiguration{ - RegistrationDrivers: []operatorapiv1.RegistrationDriverHub{ - { - AuthType: operatorapiv1.CSRAuthType, - }, - { - AuthType: operatorapiv1.GRPCAuthType, - GRPC: &operatorapiv1.GRPCRegistrationConfig{ - AutoApprovedIdentities: autoApprovedIdentities, - }, + GRPC: &operatorapiv1.GRPCRegistrationConfig{ + AutoApprovedIdentities: autoApprovedIdentities, }, }, } } + + clusterManager.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationHubConfiguration{ + RegistrationDrivers: registrationDrivers, + FeatureGates: featureGates, + } _, err = operatorClient.OperatorV1().ClusterManagers().Update(context.Background(), clusterManager, metav1.UpdateOptions{}) return err @@ -364,7 +370,17 @@ func disableGRPCAuth(operatorClient operatorclient.Interface, clusterManagerName return err } - clusterManager.Spec.RegistrationConfiguration = nil + var featureGates []operatorapiv1.FeatureGate + if clusterManager.Spec.RegistrationConfiguration != nil { + featureGates = clusterManager.Spec.RegistrationConfiguration.FeatureGates + } + if len(featureGates) == 0 { + clusterManager.Spec.RegistrationConfiguration = nil + } else { + clusterManager.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationHubConfiguration{ + FeatureGates: featureGates, + } + } _, err = operatorClient.OperatorV1().ClusterManagers().Update(context.Background(), clusterManager, metav1.UpdateOptions{}) return err diff --git a/test/integration/operator/clustermanager_hosted_test.go b/test/integration/operator/clustermanager_hosted_test.go index e34ebff4e..c9e541297 100644 --- a/test/integration/operator/clustermanager_hosted_test.go +++ b/test/integration/operator/clustermanager_hosted_test.go @@ -421,8 +421,8 @@ var _ = ginkgo.Describe("ClusterManager Hosted Mode", func() { if err != nil { return err } - if len(actual.Status.RelatedResources) != 43 { - return fmt.Errorf("should get 43 relatedResources, actual got %v, %v", + if len(actual.Status.RelatedResources) != 48 { + return fmt.Errorf("should get 48 relatedResources, actual got %v, %v", len(actual.Status.RelatedResources), actual.Status.RelatedResources) } return nil @@ -500,8 +500,8 @@ var _ = ginkgo.Describe("ClusterManager Hosted Mode", func() { if err != nil { return err } - if len(actual.Status.RelatedResources) != 47 { - return fmt.Errorf("should get 47 relatedResources, actual got %v, %v", + if len(actual.Status.RelatedResources) != 52 { + return fmt.Errorf("should get 52 relatedResources, actual got %v, %v", len(actual.Status.RelatedResources), actual.Status.RelatedResources) } return nil @@ -714,8 +714,8 @@ var _ = ginkgo.Describe("ClusterManager Hosted Mode", func() { if err != nil { return err } - if len(actual.Status.RelatedResources) != 42 { - return fmt.Errorf("should get 42 relatedResources, actual got %v, %v", + if len(actual.Status.RelatedResources) != 47 { + return fmt.Errorf("should get 47 relatedResources, actual got %v, %v", len(actual.Status.RelatedResources), actual.Status.RelatedResources) } return nil @@ -775,8 +775,8 @@ var _ = ginkgo.Describe("ClusterManager Hosted Mode", func() { if err != nil { return err } - if len(actual.Status.RelatedResources) != 47 { - return fmt.Errorf("should get 47 relatedResources, actual got %v, %v", len(actual.Status.RelatedResources), actual.Status.RelatedResources) + if len(actual.Status.RelatedResources) != 52 { + return fmt.Errorf("should get 52 relatedResources, actual got %v, %v", len(actual.Status.RelatedResources), actual.Status.RelatedResources) } return nil }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) diff --git a/test/integration/operator/clustermanager_test.go b/test/integration/operator/clustermanager_test.go index ff5732f07..8ebe56618 100644 --- a/test/integration/operator/clustermanager_test.go +++ b/test/integration/operator/clustermanager_test.go @@ -424,8 +424,8 @@ var _ = ginkgo.Describe("ClusterManager Default Mode", ginkgo.Ordered, func() { if err != nil { return err } - if len(actual.Status.RelatedResources) != 44 { - return fmt.Errorf("should get 44 relatedResources, actual got %v, %v", + if len(actual.Status.RelatedResources) != 49 { + return fmt.Errorf("should get 49 relatedResources, actual got %v, %v", len(actual.Status.RelatedResources), actual.Status.RelatedResources) } return nil @@ -503,8 +503,8 @@ var _ = ginkgo.Describe("ClusterManager Default Mode", ginkgo.Ordered, func() { if err != nil { return err } - if len(actual.Status.RelatedResources) != 48 { - return fmt.Errorf("should get 48 relatedResources, actual got %v, %v", + if len(actual.Status.RelatedResources) != 53 { + return fmt.Errorf("should get 53 relatedResources, actual got %v, %v", len(actual.Status.RelatedResources), actual.Status.RelatedResources) } return nil @@ -715,8 +715,8 @@ var _ = ginkgo.Describe("ClusterManager Default Mode", ginkgo.Ordered, func() { if err != nil { return err } - if len(actual.Status.RelatedResources) != 43 { - return fmt.Errorf("should get 43 relatedResources, actual got %v", len(actual.Status.RelatedResources)) + if len(actual.Status.RelatedResources) != 48 { + return fmt.Errorf("should get 48 relatedResources, actual got %v", len(actual.Status.RelatedResources)) } return nil }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) @@ -771,8 +771,8 @@ var _ = ginkgo.Describe("ClusterManager Default Mode", ginkgo.Ordered, func() { if err != nil { return err } - if len(actual.Status.RelatedResources) != 48 { - return fmt.Errorf("should get 48 relatedResources, actual got %v", len(actual.Status.RelatedResources)) + if len(actual.Status.RelatedResources) != 53 { + return fmt.Errorf("should get 53 relatedResources, actual got %v", len(actual.Status.RelatedResources)) } return nil }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) @@ -1147,8 +1147,8 @@ var _ = ginkgo.Describe("ClusterManager Default Mode", ginkgo.Ordered, func() { if err != nil { return err } - if len(actual.Status.RelatedResources) != 49 { - return fmt.Errorf("should get 49 relatedResources, actual got %v", len(actual.Status.RelatedResources)) + if len(actual.Status.RelatedResources) != 54 { + return fmt.Errorf("should get 54 relatedResources, actual got %v", len(actual.Status.RelatedResources)) } return nil }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) @@ -1659,7 +1659,8 @@ var _ = ginkgo.Describe("ClusterManager TLS Profile", func() { context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{}) gomega.Expect(err).NotTo(gomega.HaveOccurred()) - // Clean up PlacementConfiguration and RegistrationConfiguration to avoid affecting other tests + // Clean up PlacementConfiguration and RegistrationConfiguration to avoid affecting other tests, + // but keep any existing feature gates (e.g. NetworkPolicies) intact. gomega.Eventually(func() error { clusterManager, err := operatorClient.OperatorV1().ClusterManagers().Get( context.Background(), clusterManagerName, metav1.GetOptions{}) @@ -1667,7 +1668,17 @@ var _ = ginkgo.Describe("ClusterManager TLS Profile", func() { return err } clusterManager.Spec.PlacementConfiguration = nil - clusterManager.Spec.RegistrationConfiguration = nil + var featureGates []operatorapiv1.FeatureGate + if clusterManager.Spec.RegistrationConfiguration != nil { + featureGates = clusterManager.Spec.RegistrationConfiguration.FeatureGates + } + if len(featureGates) == 0 { + clusterManager.Spec.RegistrationConfiguration = nil + } else { + clusterManager.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationHubConfiguration{ + FeatureGates: featureGates, + } + } _, err = operatorClient.OperatorV1().ClusterManagers().Update( context.Background(), clusterManager, metav1.UpdateOptions{}) return err diff --git a/test/integration/operator/integration_suite_test.go b/test/integration/operator/integration_suite_test.go index c2ea1c999..51fbe1c55 100644 --- a/test/integration/operator/integration_suite_test.go +++ b/test/integration/operator/integration_suite_test.go @@ -138,6 +138,14 @@ var _ = ginkgo.BeforeSuite(func() { DeployOption: operatorapiv1.ClusterManagerDeployOption{ Mode: operatorapiv1.InstallModeDefault, }, + RegistrationConfiguration: &operatorapiv1.RegistrationHubConfiguration{ + FeatureGates: []operatorapiv1.FeatureGate{ + { + Feature: "NetworkPolicies", + Mode: operatorapiv1.FeatureGateModeTypeEnable, + }, + }, + }, WorkConfiguration: &operatorapiv1.WorkConfiguration{ WorkDriver: operatorapiv1.WorkDriverTypeKube, FeatureGates: []operatorapiv1.FeatureGate{ @@ -177,6 +185,14 @@ var _ = ginkgo.BeforeSuite(func() { }, }, }, + RegistrationConfiguration: &operatorapiv1.RegistrationHubConfiguration{ + FeatureGates: []operatorapiv1.FeatureGate{ + { + Feature: "NetworkPolicies", + Mode: operatorapiv1.FeatureGateModeTypeEnable, + }, + }, + }, WorkConfiguration: &operatorapiv1.WorkConfiguration{ WorkDriver: operatorapiv1.WorkDriverTypeKube, FeatureGates: []operatorapiv1.FeatureGate{