feat: add NetworkPolicies for open-cluster-management-hub namespace (#1634)

Signed-off-by: Aman Gupta <amangta.gupta@gmail.com>
This commit is contained in:
Aman Gupta
2026-08-03 08:16:39 +00:00
committed by GitHub
parent ac59d84dc7
commit f03cafff7c
26 changed files with 631 additions and 120 deletions
@@ -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"]
@@ -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"]
@@ -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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+2
View File
@@ -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 {
@@ -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:
@@ -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
@@ -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
+22
View File
@@ -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
+190
View File
@@ -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 {
@@ -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.
@@ -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)
}
})
}
}
@@ -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
}
@@ -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 {
@@ -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")
+5
View File
@@ -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")
@@ -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
@@ -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
@@ -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())
@@ -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
@@ -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{