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

1297 lines
57 KiB
Go

package operator
import (
"context"
"fmt"
"strings"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
"github.com/openshift/library-go/pkg/controller/controllercmd"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/rand"
ocmfeature "open-cluster-management.io/api/feature"
operatorapiv1 "open-cluster-management.io/api/operator/v1"
"open-cluster-management.io/ocm/pkg/operator/helpers"
"open-cluster-management.io/ocm/pkg/operator/operators/klusterlet"
"open-cluster-management.io/ocm/test/integration/util"
)
func startKlusterletOperator(ctx context.Context) {
o := &klusterlet.Options{EnableSyncLabels: true}
err := o.RunKlusterletOperator(ctx, &controllercmd.ControllerContext{
KubeConfig: restConfig,
EventRecorder: util.NewIntegrationTestEventRecorder("integration"),
})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
}
var _ = ginkgo.Describe("Klusterlet", func() {
var cancel context.CancelFunc
var klusterlet *operatorapiv1.Klusterlet
var hubKubeConfigSecret *corev1.Secret
var klusterletNamespace string
var registrationManagementRoleName string
var registrationManagedRoleName string
var addonManagementRoleName, addonManagementRoleName2 string
var workExecutionRoleName string
var workAggregateRoleName string
var workExecutionRoleBindingName string
var registrationDeploymentName string
var registrationSAName string
var workManagementRoleName string
var workManagedRoleName string
var workDeploymentName string
var workSAName string
var agentLabelSelector string
ginkgo.BeforeEach(func() {
var ctx context.Context
klusterletNamespace = fmt.Sprintf("open-cluster-management-%s", rand.String(6))
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: klusterletNamespace,
},
}
_, err := kubeClient.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
klusterlet = &operatorapiv1.Klusterlet{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("klusterlet-%s", rand.String(6)),
Labels: map[string]string{"test": "123", "component": "klusterlet", "123": "312"},
},
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",
Namespace: klusterletNamespace,
RegistrationConfiguration: &operatorapiv1.RegistrationConfiguration{
FeatureGates: []operatorapiv1.FeatureGate{
{
Feature: "NetworkPolicies",
Mode: operatorapiv1.FeatureGateModeTypeEnable,
},
},
},
},
}
agentLabelSelector = metav1.FormatLabelSelector(&metav1.LabelSelector{
MatchLabels: helpers.GetKlusterletAgentLabels(klusterlet, false),
})
hubKubeConfigSecret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: helpers.HubKubeConfig,
Namespace: klusterletNamespace,
},
Data: map[string][]byte{
"placeholder": []byte("placeholder"),
},
}
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Create(context.Background(), hubKubeConfigSecret, 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(), klusterletNamespace, 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)
addonManagementRoleName2 = fmt.Sprintf("open-cluster-management:%s-registration:addon-management", klusterlet.Name)
addonManagementRoleName = fmt.Sprintf("open-cluster-management:management:%s-registration:addon-management", klusterlet.Name)
workExecutionRoleName = fmt.Sprintf("open-cluster-management:%s-work:execution", klusterlet.Name)
workManagedRoleName = fmt.Sprintf("open-cluster-management:%s-work:agent", klusterlet.Name)
workAggregateRoleName = fmt.Sprintf("open-cluster-management:%s-work:aggregate", klusterlet.Name)
workExecutionRoleBindingName = fmt.Sprintf("open-cluster-management:%s-work:execution-admin", 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 +
// 2 CRDs + 2 deployments - 2 duplicated SAs = 28
if len(actual.Status.RelatedResources) != 28 {
return fmt.Errorf("should get 28 relatedResources, actual got %v", len(actual.Status.RelatedResources))
}
return nil
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
// Check CRDs
gomega.Eventually(func() bool {
if _, err := apiExtensionClient.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 := apiExtensionClient.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() error {
clusterRoles, err := kubeClient.RbacV1().ClusterRoles().List(context.Background(),
metav1.ListOptions{LabelSelector: agentLabelSelector})
if err != nil {
return fmt.Errorf("unable to list cluster roles: %v", err)
}
if len(clusterRoles.Items) != 6 {
return fmt.Errorf("expected 6 clusterRoles.Items, got %v", len(clusterRoles.Items))
}
for _, clusterRole := range clusterRoles.Items {
if clusterRole.GetName() != registrationManagedRoleName &&
clusterRole.GetName() != workManagedRoleName &&
clusterRole.GetName() != addonManagementRoleName &&
clusterRole.GetName() != addonManagementRoleName2 &&
clusterRole.GetName() != workExecutionRoleName &&
clusterRole.GetName() != workAggregateRoleName {
return fmt.Errorf("unexpected clusterRole %s", clusterRole.GetName())
}
}
return nil
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
gomega.Eventually(func() bool {
clusterRoleBindings, err := kubeClient.RbacV1().ClusterRoleBindings().List(context.Background(),
metav1.ListOptions{LabelSelector: agentLabelSelector})
if err != nil {
return false
}
if len(clusterRoleBindings.Items) != 6 {
return false
}
for _, clusterRoleBinding := range clusterRoleBindings.Items {
if clusterRoleBinding.GetName() != registrationManagedRoleName &&
clusterRoleBinding.GetName() != workManagedRoleName &&
clusterRoleBinding.GetName() != workExecutionRoleBindingName &&
clusterRoleBinding.GetName() != addonManagementRoleName &&
clusterRoleBinding.GetName() != addonManagementRoleName2 &&
clusterRoleBinding.GetName() != workAggregateRoleName {
return false
}
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check role/rolebinding and extension apiserver rolebinding
gomega.Eventually(func() bool {
roles, err := kubeClient.RbacV1().Roles(klusterletNamespace).List(context.Background(),
metav1.ListOptions{LabelSelector: agentLabelSelector})
if err != nil {
return false
}
if len(roles.Items) != 2 {
return false
}
for _, role := range roles.Items {
if role.GetName() != registrationManagementRoleName &&
role.GetName() != workManagementRoleName {
return false
}
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
roleBindings, err := kubeClient.RbacV1().RoleBindings(metav1.NamespaceAll).List(context.Background(),
metav1.ListOptions{LabelSelector: agentLabelSelector})
if err != nil {
return false
}
if len(roleBindings.Items) != 4 {
return false
}
for _, roleBinding := range roleBindings.Items {
if roleBinding.GetNamespace() != klusterletNamespace &&
roleBinding.GetNamespace() != "kube-system" {
return false
}
if roleBinding.GetName() != registrationManagementRoleName &&
roleBinding.GetName() != workManagementRoleName {
return false
}
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check service account
gomega.Eventually(func() bool {
serviceaccouts, err := kubeClient.CoreV1().ServiceAccounts(klusterletNamespace).List(context.Background(),
metav1.ListOptions{LabelSelector: agentLabelSelector})
if err != nil {
return false
}
if len(serviceaccouts.Items) != 2 {
return false
}
for _, serviceAccount := range serviceaccouts.Items {
if serviceAccount.GetName() != registrationSAName &&
serviceAccount.GetName() != workSAName {
return false
}
if serviceAccount.ObjectMeta.Annotations[util.IrsaAnnotationKey] == util.PrerequisiteSpokeRoleArn {
return false
}
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check deployment
gomega.Eventually(func() bool {
deployments, err := kubeClient.AppsV1().Deployments(klusterletNamespace).List(context.Background(),
metav1.ListOptions{LabelSelector: agentLabelSelector})
if err != nil {
return false
}
if len(deployments.Items) != 2 {
return false
}
for _, deployment := range deployments.Items {
if deployment.GetName() != registrationDeploymentName &&
deployment.GetName() != workDeploymentName {
return false
}
if deployment.GetName() == registrationDeploymentName {
if util.AllCommandLineOptionsPresent(deployment) || util.AwsCliSpecificVolumesMounted(deployment) {
return false
}
}
if deployment.GetName() == workDeploymentName {
if util.AwsCliSpecificVolumesMounted(deployment) {
return false
}
}
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check addon namespace
gomega.Eventually(func() bool {
namespaces, err := kubeClient.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{LabelSelector: agentLabelSelector})
if err != nil {
return false
}
if len(namespaces.Items) != 2 {
return false
}
for _, namespace := range namespaces.Items {
if namespace.GetName() != klusterletNamespace &&
namespace.GetName() != helpers.DefaultAddonNamespace {
return false
}
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, "Applied", "KlusterletApplied", metav1.ConditionTrue)
})
ginkgo.It("Deployment should be added nodeSelector and toleration when add nodePlacement into klusterlet", func() {
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(), klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
// Check deployment without nodeSelector and toleration
gomega.Eventually(func() bool {
deployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), registrationDeploymentName, metav1.GetOptions{})
if err != nil {
return false
}
if len(deployment.Spec.Template.Spec.NodeSelector) != 0 {
return false
}
if len(deployment.Spec.Template.Spec.Tolerations) != 0 {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() error {
KlusterletObj, err := operatorClient.OperatorV1().Klusterlets().Get(context.Background(), klusterlet.Name, metav1.GetOptions{})
if err != nil {
return err
}
KlusterletObj.Spec.NodePlacement = operatorapiv1.NodePlacement{
NodeSelector: map[string]string{infraNodeLabel: ""},
Tolerations: []corev1.Toleration{
{
Key: infraNodeLabel,
Operator: corev1.TolerationOpExists,
Effect: corev1.TaintEffectNoSchedule,
},
},
}
_, err = operatorClient.OperatorV1().Klusterlets().Update(context.Background(), KlusterletObj, metav1.UpdateOptions{})
return err
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeNil())
// Check deployment with nodeSelector and toleration
gomega.Eventually(func() bool {
deployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), registrationDeploymentName, metav1.GetOptions{})
if err != nil {
return false
}
if len(deployment.Spec.Template.Spec.NodeSelector) == 0 {
return false
}
if _, ok := deployment.Spec.Template.Spec.NodeSelector[infraNodeLabel]; !ok {
return false
}
if len(deployment.Spec.Template.Spec.Tolerations) == 0 {
return false
}
for _, toleration := range deployment.Spec.Template.Spec.Tolerations {
if toleration.Key == infraNodeLabel {
return true
}
}
return false
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
})
ginkgo.It("Deployment should have priorityclass configurated when setting priorityclass in klusterlet", func() {
priorityClassName := "test-priority-class"
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(), klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
// Check deployment without priorityclass
gomega.Eventually(func() bool {
deployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), registrationDeploymentName, metav1.GetOptions{})
if err != nil {
return false
}
if len(deployment.Spec.Template.Spec.PriorityClassName) != 0 {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() error {
KlusterletObj, err := operatorClient.OperatorV1().Klusterlets().Get(context.Background(), klusterlet.Name, metav1.GetOptions{})
if err != nil {
return err
}
KlusterletObj.Spec.PriorityClassName = priorityClassName
_, err = operatorClient.OperatorV1().Klusterlets().Update(context.Background(), KlusterletObj, metav1.UpdateOptions{})
return err
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeNil())
// Check deployment with priorityclass
gomega.Eventually(func() bool {
deployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), registrationDeploymentName, metav1.GetOptions{})
if err != nil {
return false
}
return deployment.Spec.Template.Spec.PriorityClassName == priorityClassName
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
})
ginkgo.It("should have correct registration deployment when server url is empty", func() {
klusterlet.Spec.ExternalServerURLs = []operatorapiv1.ServerURL{}
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(), klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Eventually(func() bool {
if _, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), registrationDeploymentName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
deployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), registrationDeploymentName, metav1.GetOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Expect(len(deployment.Spec.Template.Spec.Containers)).Should(gomega.Equal(1))
// external-server-url should not be set
for _, arg := range deployment.Spec.Template.Spec.Containers[0].Args {
gomega.Expect(strings.Contains(arg, "--spoke-external-server-urls")).NotTo(gomega.BeTrue())
}
})
ginkgo.It("should have correct work deployment until HubConnectionDegraded is False when clusterName is empty", func() {
klusterlet.Spec.ClusterName = "" // The clusterName is empty, the controller get clusterName from hubKubeConfigSecret.
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(), klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
// do not create work deployment if ClusterName is empty
gomega.Eventually(func() bool {
_, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
return errors.IsNotFound(err)
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// get correct work deployment when get valid cluster name from hub kubeConfig secret
hubSecret, err := kubeClient.CoreV1().Secrets(klusterletNamespace).Get(context.Background(), helpers.HubKubeConfig, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
// Update hub secret with cluster name
hubSecret.Data["cluster-name"] = []byte("testcluster")
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Update(context.Background(), hubSecret, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Eventually(func() bool {
if _, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
deployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Expect(len(deployment.Spec.Template.Spec.Containers)).Should(gomega.Equal(1))
for _, arg := range deployment.Spec.Template.Spec.Containers[0].Args {
if strings.HasPrefix(arg, "--spoke-cluster-name") {
gomega.Expect(arg).Should(gomega.Equal("--spoke-cluster-name=testcluster"))
}
}
// Update hub config secret to trigger work deployment update
hubSecret, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Get(context.Background(), helpers.HubKubeConfig, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
// Update hub secret
hubSecret.Data["kubeconfig"] = []byte("update dummy")
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Update(context.Background(), hubSecret, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
// Check that work deployment is updated
gomega.Eventually(func() bool {
deployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
if err != nil {
return false
}
for _, arg := range deployment.Spec.Template.Spec.Containers[0].Args {
if arg == "--spoke-cluster-name=testcluster" {
return true
}
}
return false
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
})
ginkgo.It("Should change work deployment replicas to 0 when hubConfigSecret is missing", func() {
klusterlet.Spec.ClusterName = "testcluster"
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(), klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
// replicas of work deployment should be 0 if hubConfigSecret is missing
err = kubeClient.CoreV1().Secrets(klusterletNamespace).Delete(context.Background(), helpers.HubKubeConfig, metav1.DeleteOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Eventually(func() error {
deployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
if err != nil {
return err
}
if deployment.Spec.Replicas == nil {
return err
}
if *deployment.Spec.Replicas != 0 {
return fmt.Errorf("replicas of work deployment should be 0")
}
return nil
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeNil())
// recreate the hubConfigSecret, replicas of work deployment should not be 0
hubKubeConfigSecret = &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: helpers.HubKubeConfig,
Namespace: klusterletNamespace,
},
Data: map[string][]byte{
"kubeconfig": []byte("update dummy"),
},
}
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Create(context.Background(), hubKubeConfigSecret, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Eventually(func() error {
deployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
if err != nil {
return err
}
if deployment.Spec.Replicas == nil {
return err
}
if *deployment.Spec.Replicas == 0 {
return fmt.Errorf("replicas of work deployment should not be 0")
}
return nil
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeNil())
})
ginkgo.It("Deployment should be updated when klusterlet is changed", func() {
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(), klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Eventually(func() bool {
if _, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check if generations are correct
gomega.Eventually(func() bool {
actual, err := operatorClient.OperatorV1().Klusterlets().Get(context.Background(), klusterlet.Name, metav1.GetOptions{})
if err != nil {
return false
}
if actual.Generation != actual.Status.ObservedGeneration {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() error {
klusterlet, err = operatorClient.OperatorV1().Klusterlets().Get(context.Background(), klusterlet.Name, metav1.GetOptions{})
if err != nil {
return err
}
klusterlet.Spec.ClusterName = "cluster2"
_, err = operatorClient.OperatorV1().Klusterlets().Update(context.Background(), klusterlet, metav1.UpdateOptions{})
return err
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
gomega.Eventually(func() bool {
actual, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
if err != nil {
return false
}
gomega.Expect(len(actual.Spec.Template.Spec.Containers)).Should(gomega.Equal(1))
// klusterlet has no condition, replica is 0
gomega.Expect(actual.Status.Replicas).Should(gomega.Equal(int32(0)))
// Print actual args for debugging
actualArgs := actual.Spec.Template.Spec.Containers[0].Args
if len(actualArgs) != 8 {
fmt.Fprintf(ginkgo.GinkgoWriter, "should get 8 args, actual got %v\n", actualArgs)
}
gomega.Expect(len(actualArgs)).Should(gomega.Equal(8))
return actual.Spec.Template.Spec.Containers[0].Args[2] != "--spoke-cluster-name=cluster2"
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() bool {
actual, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), registrationDeploymentName, metav1.GetOptions{})
if err != nil {
return false
}
gomega.Expect(len(actual.Spec.Template.Spec.Containers)).Should(gomega.Equal(1))
gomega.Expect(len(actual.Spec.Template.Spec.Containers[0].Args)).Should(gomega.Equal(7))
return actual.Spec.Template.Spec.Containers[0].Args[2] == "--spoke-cluster-name=cluster2"
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check if generations are correct
gomega.Eventually(func() bool {
actual, err := operatorClient.OperatorV1().Klusterlets().Get(context.Background(), klusterlet.Name, metav1.GetOptions{})
if err != nil {
return false
}
if actual.Generation != actual.Status.ObservedGeneration {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
})
ginkgo.It("Deployment should be reconciled when manually updated", func() {
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(), klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Eventually(func() bool {
if _, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
workDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
workDeployment.Spec.Template.Spec.Containers[0].Image = "test:latest"
_, err = kubeClient.AppsV1().Deployments(klusterletNamespace).Update(context.Background(), workDeployment, metav1.UpdateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Eventually(func() bool {
workDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
if err != nil {
return false
}
if workDeployment.Spec.Template.Spec.Containers[0].Image != "quay.io/open-cluster-management/work" {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check if generations are correct
gomega.Eventually(func() bool {
actual, err := operatorClient.OperatorV1().Klusterlets().Get(context.Background(), klusterlet.Name, metav1.GetOptions{})
if err != nil {
return false
}
workDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
if err != nil {
return false
}
deploymentGeneration := helpers.NewGenerationStatus(appsv1.SchemeGroupVersion.WithResource("deployments"), workDeployment)
actualGeneration := helpers.FindGenerationStatus(actual.Status.Generations, deploymentGeneration)
return deploymentGeneration.LastGeneration == actualGeneration.LastGeneration
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
})
ginkgo.It("Deployment should have correct replica", func() {
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(), klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
bootStrapSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: helpers.BootstrapHubKubeConfig,
Namespace: klusterletNamespace,
},
Data: map[string][]byte{
"kubeconfig": util.NewKubeConfig(restConfig),
},
}
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Create(context.Background(), bootStrapSecret, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
// Update hub secret with cluster name and kubeconfig
hubSecret, err := kubeClient.CoreV1().Secrets(klusterletNamespace).Get(context.Background(), helpers.HubKubeConfig, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
hubSecret.Data["cluster-name"] = []byte("testcluster")
hubSecret.Data["kubeconfig"] = util.NewKubeConfig(restConfig)
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Update(context.Background(), hubSecret, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
// Expect 1 replica since no nodes exists currently
gomega.Eventually(func() int32 {
workDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
if err != nil {
return 0
}
return *workDeployment.Spec.Replicas
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Equal(int32(1)))
// Create master nodes and recreate klusterlet
_, err = kubeClient.CoreV1().Nodes().Create(
context.Background(),
&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node1", Labels: map[string]string{"node-role.kubernetes.io/master": ""}}},
metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
_, err = kubeClient.CoreV1().Nodes().Create(
context.Background(),
&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node2", Labels: map[string]string{"node-role.kubernetes.io/master": ""}}},
metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
_, err = kubeClient.CoreV1().Nodes().Create(
context.Background(),
&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node3", Labels: map[string]string{"node-role.kubernetes.io/master": ""}}},
metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
// update klusterlet to trigger another reconcile
klusterlet, err = operatorClient.OperatorV1().Klusterlets().Get(context.Background(), klusterlet.Name, metav1.GetOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
klusterlet.Labels = map[string]string{"test": "test"}
_, err = operatorClient.OperatorV1().Klusterlets().Update(context.Background(), klusterlet, metav1.UpdateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Eventually(func() error {
workDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
if err != nil {
return err
}
if *workDeployment.Spec.Replicas != 3 {
return fmt.Errorf("expect 3 deployment but got %d", *workDeployment.Spec.Replicas)
}
return nil
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
})
ginkgo.It("Deployment should be added hostAliases when add hostAlias into klusterlet", func() {
var hubApiServerHostAliasIP = "11.22.33.44"
var hubApiServerHostAliasHostname = "open-cluster-management.io"
klusterlet.Spec.HubApiServerHostAlias = &operatorapiv1.HubApiServerHostAlias{
IP: hubApiServerHostAliasIP,
Hostname: hubApiServerHostAliasHostname,
}
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(), klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
// Check registration agent deployment
gomega.Eventually(func() bool {
actual, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), registrationDeploymentName, metav1.GetOptions{})
if err != nil {
return false
}
gomega.Expect(len(actual.Spec.Template.Spec.HostAliases)).Should(gomega.Equal(1))
gomega.Expect(actual.Spec.Template.Spec.HostAliases[0].IP).Should(gomega.Equal(hubApiServerHostAliasIP))
gomega.Expect(len(actual.Spec.Template.Spec.HostAliases[0].Hostnames)).Should(gomega.Equal(1))
gomega.Expect(actual.Spec.Template.Spec.HostAliases[0].Hostnames[0]).Should(gomega.Equal(hubApiServerHostAliasHostname))
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check work agent deployment
gomega.Eventually(func() bool {
actual, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
if err != nil {
return false
}
gomega.Expect(len(actual.Spec.Template.Spec.HostAliases)).Should(gomega.Equal(1))
gomega.Expect(actual.Spec.Template.Spec.HostAliases[0].IP).Should(gomega.Equal(hubApiServerHostAliasIP))
gomega.Expect(len(actual.Spec.Template.Spec.HostAliases[0].Hostnames)).Should(gomega.Equal(1))
gomega.Expect(actual.Spec.Template.Spec.HostAliases[0].Hostnames[0]).Should(gomega.Equal(hubApiServerHostAliasHostname))
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
})
ginkgo.It("Deployment should be updated when klusterlet claim configure is changed", func() {
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(), klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Eventually(func() bool {
if _, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check if generations are correct
gomega.Eventually(func() bool {
actual, err := operatorClient.OperatorV1().Klusterlets().Get(context.Background(), klusterlet.Name, metav1.GetOptions{})
if err != nil {
return false
}
if actual.Generation != actual.Status.ObservedGeneration {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
gomega.Eventually(func() error {
klusterlet, err = operatorClient.OperatorV1().Klusterlets().Get(context.Background(), klusterlet.Name, metav1.GetOptions{})
if err != nil {
return err
}
klusterlet.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationConfiguration{
ClusterClaimConfiguration: &operatorapiv1.ClusterClaimConfiguration{
MaxCustomClusterClaims: 2,
ReservedClusterClaimSuffixes: []string{"reserved1.io", "reserved2.io"},
},
}
_, err = operatorClient.OperatorV1().Klusterlets().Update(context.Background(), klusterlet, metav1.UpdateOptions{})
return err
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
gomega.Eventually(func() bool {
actual, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), registrationDeploymentName, metav1.GetOptions{})
if err != nil {
return false
}
gomega.Expect(len(actual.Spec.Template.Spec.Containers)).Should(gomega.Equal(1))
if len(actual.Spec.Template.Spec.Containers[0].Args) != 9 {
return false
}
return actual.Spec.Template.Spec.Containers[0].Args[7] == "--max-custom-cluster-claims=2" &&
actual.Spec.Template.Spec.Containers[0].Args[8] == "--reserved-cluster-claim-suffixes=reserved1.io,reserved2.io"
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
// Check if generations are correct
gomega.Eventually(func() bool {
actual, err := operatorClient.OperatorV1().Klusterlets().Get(context.Background(), klusterlet.Name, metav1.GetOptions{})
if err != nil {
return false
}
if actual.Generation != actual.Status.ObservedGeneration {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
})
})
ginkgo.Context("klusterlet statuses", func() {
ginkgo.BeforeEach(func() {
registrationDeploymentName = fmt.Sprintf("%s-registration-agent", klusterlet.Name)
workDeploymentName = fmt.Sprintf("%s-work-agent", klusterlet.Name)
})
ginkgo.AfterEach(func() {
gomega.Expect(operatorClient.OperatorV1().Klusterlets().Delete(context.Background(), klusterlet.Name, metav1.DeleteOptions{})).To(gomega.BeNil())
})
ginkgo.It("should have correct degraded conditions", func() {
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(), klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, "HubConnectionDegraded", "BootstrapSecretMissing,HubKubeConfigMissing", metav1.ConditionTrue)
// Create a bootstrap secret and make sure the kubeconfig can work
bootStrapSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: helpers.BootstrapHubKubeConfig,
Namespace: klusterletNamespace,
},
Data: map[string][]byte{
"kubeconfig": util.NewKubeConfig(restConfig),
},
}
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Create(context.Background(), bootStrapSecret, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
util.AssertKlusterletCondition(
klusterlet.Name, operatorClient, "HubConnectionDegraded", "BootstrapSecretFunctional,HubKubeConfigMissing", metav1.ConditionTrue)
hubSecret, err := kubeClient.CoreV1().Secrets(klusterletNamespace).Get(context.Background(), helpers.HubKubeConfig, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
// Update hub secret and make sure the kubeconfig can work
hubSecret = hubSecret.DeepCopy()
hubSecret.Data["cluster-name"] = []byte("testcluster")
hubSecret.Data["kubeconfig"] = util.NewKubeConfig(restConfig)
hubSecret, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Update(context.Background(), hubSecret, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, "HubConnectionDegraded", "HubConnectionFunctional", metav1.ConditionFalse)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, "Applied", "KlusterletApplied", metav1.ConditionTrue)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, "RegistrationDesiredDegraded", "UnavailablePods", metav1.ConditionTrue)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, "WorkDesiredDegraded", "UnavailablePods", metav1.ConditionTrue)
// Update replica of deployment
registrationDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(
context.Background(), registrationDeploymentName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
registrationDeployment = registrationDeployment.DeepCopy()
registrationDeployment.Status.AvailableReplicas = 3
registrationDeployment.Status.Replicas = 3
registrationDeployment.Status.ReadyReplicas = 3
_, err = kubeClient.AppsV1().Deployments(klusterletNamespace).UpdateStatus(context.Background(), registrationDeployment, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
workDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
workDeployment = workDeployment.DeepCopy()
workDeployment.Status.AvailableReplicas = 3
workDeployment.Status.Replicas = 3
workDeployment.Status.ReadyReplicas = 3
_, err = kubeClient.AppsV1().Deployments(klusterletNamespace).UpdateStatus(context.Background(), workDeployment, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, "HubConnectionDegraded", "HubConnectionFunctional", metav1.ConditionFalse)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, "RegistrationDesiredDegraded", "DeploymentsFunctional", metav1.ConditionFalse)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, "WorkDesiredDegraded", "DeploymentsFunctional", metav1.ConditionFalse)
// Delete the hub kubeconfig secret
err = kubeClient.CoreV1().Secrets(klusterletNamespace).Delete(context.Background(), hubSecret.Name, metav1.DeleteOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
// Update replica of deployment
registrationDeployment, err = kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), registrationDeploymentName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
registrationDeployment = registrationDeployment.DeepCopy()
registrationDeployment.Status.AvailableReplicas = 0
_, err = kubeClient.AppsV1().Deployments(klusterletNamespace).UpdateStatus(context.Background(), registrationDeployment, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
util.AssertKlusterletCondition(
klusterlet.Name, operatorClient,
"HubConnectionDegraded",
"BootstrapSecretFunctional,HubKubeConfigSecretMissing", metav1.ConditionTrue)
})
ginkgo.It("should have correct available conditions", func() {
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(), klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Eventually(func() bool {
if _, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), registrationDeploymentName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
registrationDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(
context.Background(), registrationDeploymentName, metav1.GetOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Eventually(func() bool {
if _, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{}); err != nil {
return false
}
return true
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
workDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(context.Background(), workDeploymentName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, "Available", "NoAvailablePods", metav1.ConditionFalse)
// Update replica of deployment, more than 0 AvailableReplicas makes the Available=true
registrationDeployment.Status.AvailableReplicas = 1
registrationDeployment.Status.Replicas = 3
registrationDeployment.Status.ReadyReplicas = 3
_, err = kubeClient.AppsV1().Deployments(klusterletNamespace).UpdateStatus(context.Background(), registrationDeployment, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
workDeployment.Status.AvailableReplicas = 1
workDeployment.Status.Replicas = 3
workDeployment.Status.ReadyReplicas = 3
_, err = kubeClient.AppsV1().Deployments(klusterletNamespace).UpdateStatus(context.Background(), workDeployment, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, "Available", "KlusterletAvailable", metav1.ConditionTrue)
})
})
ginkgo.Context("klusterlet feature gates and configuration", func() {
ginkgo.BeforeEach(func() {
registrationDeploymentName = fmt.Sprintf("%s-registration-agent", klusterlet.Name)
workDeploymentName = fmt.Sprintf("%s-work-agent", klusterlet.Name)
})
ginkgo.AfterEach(func() {
gomega.Expect(operatorClient.OperatorV1().Klusterlets().Delete(context.Background(),
klusterlet.Name, metav1.DeleteOptions{})).To(gomega.BeNil())
})
ginkgo.It("feature gates configuration is nil or empty", func() {
klusterlet.Spec.RegistrationConfiguration = nil
klusterlet.Spec.WorkConfiguration = &operatorapiv1.WorkAgentConfiguration{}
ginkgo.By("Create the klusterlet with RegistrationConfiguration nil and WorkConfiguration empty")
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(),
klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
ginkgo.By("Create a bootstrap secret and make sure the kubeconfig can work")
bootStrapSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: helpers.BootstrapHubKubeConfig,
Namespace: klusterletNamespace,
},
Data: map[string][]byte{
"kubeconfig": util.NewKubeConfig(restConfig),
},
}
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Create(context.Background(),
bootStrapSecret, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
hubSecret, err := kubeClient.CoreV1().Secrets(klusterletNamespace).Get(context.Background(),
helpers.HubKubeConfig, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
ginkgo.By("Update hub secret and make sure the kubeconfig can work")
hubSecret = hubSecret.DeepCopy()
hubSecret.Data["cluster-name"] = []byte("testcluster")
hubSecret.Data["kubeconfig"] = util.NewKubeConfig(restConfig)
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Update(context.Background(),
hubSecret, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
util.AssertKlusterletCondition(klusterlet.Name, operatorClient,
"HubConnectionDegraded", "HubConnectionFunctional", metav1.ConditionFalse)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient,
"RegistrationDesiredDegraded", "UnavailablePods", metav1.ConditionTrue)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient,
"WorkDesiredDegraded", "UnavailablePods", metav1.ConditionTrue)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, helpers.FeatureGatesTypeValid,
helpers.FeatureGatesReasonAllValid, metav1.ConditionTrue)
ginkgo.By("Check the registration-agent has the expected feature gates")
registrationDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(
context.Background(), registrationDeploymentName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(registrationDeployment.Spec.Template.Spec.Containers[0].Args).ShouldNot(
gomega.ContainElement("--feature-gates=AddonManagement=true"))
ginkgo.By("Check the work-agent has the expected feature gates")
workDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(
context.Background(), workDeploymentName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(workDeployment.Spec.Template.Spec.Containers[0].Args).ShouldNot(
gomega.ContainElement("--feature-gates="))
})
ginkgo.It("should set certDurationSeconds correctly", func() {
klusterlet.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationConfiguration{
ClientCertExpirationSeconds: 120,
}
ginkgo.By("Create the klusterlet with valid RegistrationConfiguration")
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(),
klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
ginkgo.By("Create a bootstrap secret and make sure the kubeconfig can work")
bootStrapSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: helpers.BootstrapHubKubeConfig,
Namespace: klusterletNamespace,
},
Data: map[string][]byte{
"kubeconfig": util.NewKubeConfig(restConfig),
},
}
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Create(context.Background(),
bootStrapSecret, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
hubSecret, err := kubeClient.CoreV1().Secrets(klusterletNamespace).Get(context.Background(),
helpers.HubKubeConfig, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
ginkgo.By("Update hub secret and make sure the kubeconfig can work")
hubSecret = hubSecret.DeepCopy()
hubSecret.Data["cluster-name"] = []byte("testcluster")
hubSecret.Data["kubeconfig"] = util.NewKubeConfig(restConfig)
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Update(context.Background(),
hubSecret, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
util.AssertKlusterletCondition(klusterlet.Name, operatorClient,
"HubConnectionDegraded", "HubConnectionFunctional", metav1.ConditionFalse)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient,
"RegistrationDesiredDegraded", "UnavailablePods", metav1.ConditionTrue)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient,
"WorkDesiredDegraded", "UnavailablePods", metav1.ConditionTrue)
ginkgo.By("Check the registration-agent has the expected agrs")
registrationDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(
context.Background(), registrationDeploymentName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(registrationDeployment.Spec.Template.Spec.Containers[0].Args).Should(
gomega.ContainElement("--client-cert-expiration-seconds=120"))
})
ginkgo.It("should be set correctly", func() {
klusterlet.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationConfiguration{
FeatureGates: []operatorapiv1.FeatureGate{
{
Feature: string(ocmfeature.ClusterClaim),
Mode: operatorapiv1.FeatureGateModeTypeDisable,
},
},
ClusterAnnotations: map[string]string{
"foo": "bar", // should be ignored
"agent.open-cluster-management.io/foo": "bar",
},
}
klusterlet.Spec.WorkConfiguration = &operatorapiv1.WorkAgentConfiguration{
FeatureGates: []operatorapiv1.FeatureGate{
{
Feature: string(ocmfeature.ExecutorValidatingCaches),
Mode: operatorapiv1.FeatureGateModeTypeEnable,
},
},
}
ginkgo.By("Create the klusterlet with valid RegistrationConfiguration and WorkConfiguration")
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(),
klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
ginkgo.By("Create a bootstrap secret and make sure the kubeconfig can work")
bootStrapSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: helpers.BootstrapHubKubeConfig,
Namespace: klusterletNamespace,
},
Data: map[string][]byte{
"kubeconfig": util.NewKubeConfig(restConfig),
},
}
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Create(context.Background(),
bootStrapSecret, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
hubSecret, err := kubeClient.CoreV1().Secrets(klusterletNamespace).Get(context.Background(),
helpers.HubKubeConfig, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
ginkgo.By("Update hub secret and make sure the kubeconfig can work")
hubSecret = hubSecret.DeepCopy()
hubSecret.Data["cluster-name"] = []byte("testcluster")
hubSecret.Data["kubeconfig"] = util.NewKubeConfig(restConfig)
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Update(context.Background(),
hubSecret, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
util.AssertKlusterletCondition(klusterlet.Name, operatorClient,
"HubConnectionDegraded", "HubConnectionFunctional", metav1.ConditionFalse)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient,
"RegistrationDesiredDegraded", "UnavailablePods", metav1.ConditionTrue)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient,
"WorkDesiredDegraded", "UnavailablePods", metav1.ConditionTrue)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, helpers.FeatureGatesTypeValid,
helpers.FeatureGatesReasonAllValid, metav1.ConditionTrue)
ginkgo.By("Check the registration-agent has the expected feature gates")
registrationDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(
context.Background(), registrationDeploymentName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(registrationDeployment.Spec.Template.Spec.Containers[0].Args).Should(
gomega.ContainElement("--feature-gates=ClusterClaim=false"))
ginkgo.By("Check the registration-agent has the expected cluster-annotations")
gomega.Expect(registrationDeployment.Spec.Template.Spec.Containers[0].Args).Should(
gomega.ContainElement("--cluster-annotations=agent.open-cluster-management.io/foo=bar"))
ginkgo.By("Check the work-agent has the expected feature gates")
workDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(
context.Background(), workDeploymentName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(workDeployment.Spec.Template.Spec.Containers[0].Args).Should(
gomega.ContainElement("--feature-gates=ExecutorValidatingCaches=true"))
})
ginkgo.It("has invalid feature gates", func() {
klusterlet.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationConfiguration{
FeatureGates: []operatorapiv1.FeatureGate{
{
Feature: "Foo",
Mode: operatorapiv1.FeatureGateModeTypeDisable,
},
{
Feature: string(ocmfeature.ClusterClaim),
Mode: operatorapiv1.FeatureGateModeTypeDisable,
},
},
}
klusterlet.Spec.WorkConfiguration = &operatorapiv1.WorkAgentConfiguration{
FeatureGates: []operatorapiv1.FeatureGate{
{
Feature: "Bar",
Mode: operatorapiv1.FeatureGateModeTypeEnable,
},
{
Feature: string(ocmfeature.ExecutorValidatingCaches),
Mode: operatorapiv1.FeatureGateModeTypeEnable,
},
},
}
ginkgo.By("Create the klusterlet with invalid RegistrationConfiguration and WorkConfiguration")
_, err := operatorClient.OperatorV1().Klusterlets().Create(context.Background(),
klusterlet, metav1.CreateOptions{})
gomega.Expect(err).NotTo(gomega.HaveOccurred())
ginkgo.By("Create a bootstrap secret and make sure the kubeconfig can work")
bootStrapSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: helpers.BootstrapHubKubeConfig,
Namespace: klusterletNamespace,
},
Data: map[string][]byte{
"kubeconfig": util.NewKubeConfig(restConfig),
},
}
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Create(context.Background(),
bootStrapSecret, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
hubSecret, err := kubeClient.CoreV1().Secrets(klusterletNamespace).Get(context.Background(),
helpers.HubKubeConfig, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
ginkgo.By("Update hub secret and make sure the kubeconfig can work")
hubSecret = hubSecret.DeepCopy()
hubSecret.Data["cluster-name"] = []byte("testcluster")
hubSecret.Data["kubeconfig"] = util.NewKubeConfig(restConfig)
_, err = kubeClient.CoreV1().Secrets(klusterletNamespace).Update(context.Background(),
hubSecret, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
util.AssertKlusterletCondition(klusterlet.Name, operatorClient,
"HubConnectionDegraded", "HubConnectionFunctional", metav1.ConditionFalse)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient,
"RegistrationDesiredDegraded", "UnavailablePods", metav1.ConditionTrue)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient,
"WorkDesiredDegraded", "UnavailablePods", metav1.ConditionTrue)
util.AssertKlusterletCondition(klusterlet.Name, operatorClient, helpers.FeatureGatesTypeValid,
helpers.FeatureGatesReasonInvalidExisting, metav1.ConditionFalse)
ginkgo.By("Check the registration-agent only have the valid feature gates")
registrationDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(
context.Background(), registrationDeploymentName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(registrationDeployment.Spec.Template.Spec.Containers[0].Args).Should(
gomega.ContainElement("--feature-gates=ClusterClaim=false"))
gomega.Expect(registrationDeployment.Spec.Template.Spec.Containers[0].Args).ShouldNot(
gomega.ContainElement("--feature-gates=Foo=false"))
ginkgo.By("Check the work-agent only have the valid feature gates")
workDeployment, err := kubeClient.AppsV1().Deployments(klusterletNamespace).Get(
context.Background(), workDeploymentName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(workDeployment.Spec.Template.Spec.Containers[0].Args).Should(
gomega.ContainElement("--feature-gates=ExecutorValidatingCaches=true"))
gomega.Expect(workDeployment.Spec.Template.Spec.Containers[0].Args).ShouldNot(
gomega.ContainElement("--feature-gates=Bar=true"))
})
})
})