Files
mihirleleandClaude Opus 4.6 374cc31e22 feat: add NetworkPolicies for open-cluster-management-agent namespace (#1627)
* feat: add NetworkPolicies for open-cluster-management-agent namespace

Ship 4 NetworkPolicy manifests with the klusterlet operator to restrict
ingress/egress in the agent namespace, meeting CIS Kube benchmark 5.3.2
requirements.

Policies added:
- default-deny-all: baseline deny for all ingress/egress
- allow-dns-and-api: DNS egress (OpenShift/kube-dns) + ports-only API
  server egress (TCP 443/6443)
- klusterlet: operator egress to intra-namespace, addon namespace, and
  kubernetes.default.svc
- klusterlet-agent: agent egress to kubernetes.default.svc, hub webhooks,
  and intra-namespace

Also adds:
- RBAC: networking.k8s.io/networkpolicies permissions in klusterlet
  ClusterRole
- Cleanup: *networkingv1.NetworkPolicy case in CleanUpStaticObject and
  GenerateRelatedResource

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Mihir Lele <mlele@redhat.com>

* feat: add NetworkPolicies feature gate and fix ingress rules

- Add NetworkPolicies feature gate (disabled by default) following the
  existing AboutAPIEnabled/ClusterProperty pattern
- Filter operator-internal feature gates before ConvertToFeatureGateFlags
  to avoid invalid agent CLI flags and ValidFeatureGates condition issues
- Conditionally apply NP manifests in both reconcile() and clean() based
  on the feature gate
- Fix missing intra-namespace ingress rules in klusterlet and
  klusterlet-agent NetworkPolicies (CodeRabbit review feedback)
- Revert test counts to pre-NP values and add
  TestSyncDeployWithNetworkPolicies for the feature-enabled path

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Mihir Lele <mlele@redhat.com>

* fix: address CodeRabbit nitpicks - import grouping and defensive slice copy

- Move featuregate import to correct alphabetical position in its group
- Use defensive slice copy (append into new slice) instead of direct
  assignment from package-level var before appending

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Mihir Lele <mlele@redhat.com>

* fix: update OLM CSV with NetworkPolicy RBAC permissions

The ClusterServiceVersion file needs to include the networking.k8s.io
networkpolicies RBAC rules added to the klusterlet ClusterRole.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Mihir Lele <mlele@redhat.com>

* test: enable NetworkPolicies feature gate in integration and e2e tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Mihir Lele <mlele@redhat.com>

* test: update relatedResources counts for NetworkPolicy resources

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Mihir Lele <mlele@redhat.com>

* fix: update expected registration deployment args count for RegistrationConfiguration defaults

RegistrationConfiguration triggers kubebuilder defaults (KubeAPIQPS=50,
KubeAPIBurst=100), adding --kube-api-qps and --kube-api-burst args.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Mihir Lele <mlele@redhat.com>

* fix: clarify relatedResources count comment arithmetic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Mihir Lele <mlele@redhat.com>

---------

Signed-off-by: Mihir Lele <mlele@redhat.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-24 01:30:13 +00:00

267 lines
11 KiB
Go

package operator
import (
"context"
"fmt"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/rand"
operatorapiv1 "open-cluster-management.io/api/operator/v1"
"open-cluster-management.io/ocm/pkg/operator/helpers"
"open-cluster-management.io/ocm/test/integration/util"
)
var _ = ginkgo.Describe("Klusterlet Hosted mode", func() {
var cancel context.CancelFunc
var klusterlet *operatorapiv1.Klusterlet
var klusterletNamespace string
var agentNamespace string
var registrationManagementRoleName string
var registrationManagedRoleName string
var registrationDeploymentName string
var registrationSAName string
var workManagementRoleName string
var workManagedRoleName string
var workDeploymentName string
var workSAName string
ginkgo.BeforeEach(func() {
var ctx context.Context
klusterlet = &operatorapiv1.Klusterlet{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("klusterlet-%s", rand.String(6)),
},
Spec: operatorapiv1.KlusterletSpec{
RegistrationImagePullSpec: "quay.io/open-cluster-management/registration",
WorkImagePullSpec: "quay.io/open-cluster-management/work",
ExternalServerURLs: []operatorapiv1.ServerURL{
{
URL: "https://localhost",
},
},
ClusterName: "testcluster",
DeployOption: operatorapiv1.KlusterletDeployOption{
Mode: operatorapiv1.InstallModeHosted,
},
RegistrationConfiguration: &operatorapiv1.RegistrationConfiguration{
FeatureGates: []operatorapiv1.FeatureGate{
{
Feature: "NetworkPolicies",
Mode: operatorapiv1.FeatureGateModeTypeEnable,
},
},
},
},
}
klusterletNamespace = helpers.KlusterletNamespace(klusterlet)
agentNamespace = helpers.AgentNamespace(klusterlet)
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: agentNamespace,
},
}
_, err := kubeClient.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
// Create the external managed kubeconfig secret
managedKubeconfigSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: helpers.ExternalManagedKubeConfig,
Namespace: agentNamespace,
},
Data: map[string][]byte{
"kubeconfig": util.NewKubeConfig(hostedRestConfig),
},
}
_, err = kubeClient.CoreV1().Secrets(agentNamespace).Create(context.Background(), managedKubeconfigSecret, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
ctx, cancel = context.WithCancel(context.Background())
go startKlusterletOperator(ctx)
})
ginkgo.AfterEach(func() {
err := kubeClient.CoreV1().Namespaces().Delete(context.Background(), agentNamespace, metav1.DeleteOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
if cancel != nil {
cancel()
}
})
ginkgo.Context("Deploy and clean klusterlet component", func() {
ginkgo.BeforeEach(func() {
registrationDeploymentName = fmt.Sprintf("%s-registration-agent", klusterlet.Name)
workDeploymentName = fmt.Sprintf("%s-work-agent", klusterlet.Name)
registrationManagementRoleName = fmt.Sprintf("open-cluster-management:management:%s-registration:agent", klusterlet.Name)
workManagementRoleName = fmt.Sprintf("open-cluster-management:management:%s-work:agent", klusterlet.Name)
registrationManagedRoleName = fmt.Sprintf("open-cluster-management:%s-registration:agent", klusterlet.Name)
workManagedRoleName = fmt.Sprintf("open-cluster-management:%s-work:agent", klusterlet.Name)
registrationSAName = fmt.Sprintf("%s-registration-sa", klusterlet.Name)
workSAName = fmt.Sprintf("%s-work-sa", klusterlet.Name)
})
ginkgo.AfterEach(func() {
gomega.Expect(operatorClient.OperatorV1().Klusterlets().Delete(context.Background(), klusterlet.Name, metav1.DeleteOptions{})).To(gomega.BeNil())
})
ginkgo.It("should have expected resource created successfully", func() {
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(), klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
// Check if relatedResources are correct
gomega.Eventually(func() error {
actual, err := operatorClient.OperatorV1().Klusterlets().Get(context.Background(), klusterlet.Name, metav1.GetOptions{})
if err != nil {
return err
}
// 11 managed static manifests + 11 management static manifests + 4 networkpolicies +
// 2CRDs + 2 deployments(2 duplicated CRDs, but status also recorded in the klusterlet's status)
if len(actual.Status.RelatedResources) != 30 {
return fmt.Errorf("should get 30 relatedResources, actual got %v", len(actual.Status.RelatedResources))
}
return nil
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
// Check CRDs
gomega.Eventually(func() bool {
if _, err := hostedAPIExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(
context.Background(), "appliedmanifestworks.work.open-cluster-management.io", metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
if _, err := hostedAPIExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(
context.Background(), "clusterclaims.cluster.open-cluster-management.io", metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check clusterrole/clusterrolebinding
gomega.Eventually(func() bool {
if _, err := hostedKubeClient.RbacV1().ClusterRoles().Get(
context.Background(), registrationManagedRoleName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
if _, err := hostedKubeClient.RbacV1().ClusterRoles().Get(context.Background(), workManagedRoleName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
if _, err := hostedKubeClient.RbacV1().ClusterRoleBindings().Get(context.Background(), registrationManagedRoleName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
if _, err := hostedKubeClient.RbacV1().ClusterRoleBindings().Get(context.Background(), workManagedRoleName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check role/rolebinding
gomega.Eventually(func() bool {
if _, err := kubeClient.RbacV1().Roles(agentNamespace).Get(context.Background(), registrationManagementRoleName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
if _, err := kubeClient.RbacV1().Roles(agentNamespace).Get(context.Background(), workManagementRoleName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
if _, err := kubeClient.RbacV1().RoleBindings(agentNamespace).Get(context.Background(), registrationManagementRoleName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
if _, err := kubeClient.RbacV1().RoleBindings(agentNamespace).Get(context.Background(), workManagementRoleName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check extension apiserver rolebinding
gomega.Eventually(func() bool {
if _, err := kubeClient.RbacV1().RoleBindings("kube-system").Get(context.Background(), registrationManagementRoleName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
if _, err := kubeClient.RbacV1().RoleBindings("kube-system").Get(context.Background(), workManagementRoleName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check service account
gomega.Eventually(func() bool {
if _, err := kubeClient.CoreV1().ServiceAccounts(agentNamespace).Get(context.Background(), registrationSAName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
if _, err := kubeClient.CoreV1().ServiceAccounts(agentNamespace).Get(context.Background(), workSAName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
if _, err := hostedKubeClient.CoreV1().ServiceAccounts(klusterletNamespace).Get(context.Background(), registrationSAName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
if _, err := hostedKubeClient.CoreV1().ServiceAccounts(klusterletNamespace).Get(context.Background(), workSAName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check deployment
gomega.Eventually(func() bool {
if _, err := kubeClient.AppsV1().Deployments(agentNamespace).Get(context.Background(), registrationDeploymentName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
if _, err := kubeClient.AppsV1().Deployments(agentNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check addon namespace
gomega.Eventually(func() bool {
if _, err := hostedKubeClient.CoreV1().Namespaces().Get(context.Background(), helpers.DefaultAddonNamespace, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, "Applied", "KlusterletApplied", metav1.ConditionTrue)
})
})
})