mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-08-19 20:26:48 +00:00
* test: add v1beta1 e2e tests for addon lifecycle and install strategy Add v1beta1 e2e test coverage following the alpha/beta split pattern from integration tests. Rename framework helper functions to use explicit V1Alpha1/V1Beta1 suffixes for consistency. Framework changes (test/framework/managedclusteraddon.go): - Rename CreateManagedClusterAddOn to CreateManagedClusterAddOnV1Alpha1 - Rename CheckManagedClusterAddOnStatus to CheckManagedClusterAddOnStatusV1Alpha1 - Add CheckManagedClusterAddOnStatusV1Beta1 helper - Organize functions with Alpha version followed by Beta counterpart E2E test changes: - Rename addon_test.go to addon_alpha_test.go (v1alpha1 API) - Create new addon_test.go with v1beta1 API using V1Beta1 helpers - Rename addon_install_test.go to addon_install_alpha_test.go (v1alpha1 API) - Create new addon_install_test.go with v1beta1 API - Update addon_lease_test.go, addon_token_auth_test.go, addonmanagement_test.go to use V1Alpha1 helpers Test coverage (both alpha and v1beta1): - addon_test.go: Basic ManagedClusterAddOn lifecycle and availability - addon_install_test.go: ClusterManagementAddOn with placement-based install strategy, addon annotation syncing Naming pattern matches integration tests: *_alpha_test.go for v1alpha1, *_test.go (no suffix) for v1beta1. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Signed-off-by: Tesshu Flower <tflower@redhat.com> * test: add v1beta1 e2e tests for addon lease health check Add v1beta1 e2e test coverage for addon lease-based health checking following the alpha/beta split pattern. Changes: - Rename addon_lease_test.go to addon_lease_alpha_test.go (v1alpha1 API) - Create new addon_lease_test.go with v1beta1 API using V1Beta1 helpers - Update all AddonClient.AddonV1alpha1() calls to AddonV1beta1() in beta test Both alpha and v1beta1 tests cover: - Addon status remains available while lease is updated - Addon status changes to unavailable when lease stops updating - Addon status changes to unknown when no lease exists - Addon status changes to unknown when managed cluster lease stops updating Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Signed-off-by: Tesshu Flower <tflower@redhat.com> * test: migrate addon_token_auth test to alpha/beta versions - Rename addon_token_auth_test.go to addon_token_auth_alpha_test.go - Update alpha Describe to include (v1alpha1) - Create new addon_token_auth_test.go for v1beta1 - Update imports and client calls to use v1beta1 API Signed-off-by: Tesshu Flower <tflower@redhat.com> --------- Signed-off-by: Tesshu Flower <tflower@redhat.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
360 lines
14 KiB
Go
360 lines
14 KiB
Go
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
ginkgo "github.com/onsi/ginkgo/v2"
|
|
"github.com/onsi/gomega"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/equality"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/util/rand"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/kubernetes/scheme"
|
|
|
|
addonapiv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
|
|
clusterv1 "open-cluster-management.io/api/cluster/v1"
|
|
operatorapiv1 "open-cluster-management.io/api/operator/v1"
|
|
|
|
"open-cluster-management.io/ocm/pkg/addon/templateagent"
|
|
"open-cluster-management.io/ocm/test/e2e/manifests"
|
|
)
|
|
|
|
var _ = ginkgo.Describe("Template addon with token-based authentication (v1alpha1)", ginkgo.Ordered, ginkgo.Label("addon-manager", "addon-token-auth"), func() {
|
|
addOnName := "hello-template"
|
|
addonInstallNamespace := "test-addon-template-token"
|
|
var signerSecretNamespace string
|
|
var originalAddOnDriver *operatorapiv1.AddOnRegistrationDriver
|
|
|
|
var agentClient kubernetes.Interface
|
|
var agentNamespace string
|
|
s := runtime.NewScheme()
|
|
_ = scheme.AddToScheme(s)
|
|
_ = addonapiv1alpha1.Install(s)
|
|
_ = clusterv1.Install(s)
|
|
|
|
templateResources := []string{
|
|
"addon/addon_template.yaml",
|
|
"addon/cluster_management_addon.yaml",
|
|
"addon/cluster_role.yaml",
|
|
"addon/signca_secret_role.yaml",
|
|
"addon/signca_secret_rolebinding.yaml",
|
|
}
|
|
|
|
ginkgo.BeforeAll(func() {
|
|
ginkgo.By("Save original klusterlet configuration")
|
|
klusterlet, err := spoke.OperatorClient.OperatorV1().Klusterlets().Get(
|
|
context.TODO(), universalKlusterletName, metav1.GetOptions{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
if klusterlet.Spec.RegistrationConfiguration != nil {
|
|
originalAddOnDriver = klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver
|
|
}
|
|
|
|
ginkgo.By("Get initial registration agent deployment generation before updating klusterlet")
|
|
var initialGeneration int64
|
|
var registrationDeploymentName string
|
|
registrationDeploymentName = fmt.Sprintf("%s-registration-agent", klusterlet.Name)
|
|
if klusterlet.Spec.DeployOption.Mode == operatorapiv1.InstallModeSingleton ||
|
|
klusterlet.Spec.DeployOption.Mode == operatorapiv1.InstallModeSingletonHosted {
|
|
registrationDeploymentName = fmt.Sprintf("%s-agent", klusterlet.Name)
|
|
}
|
|
|
|
// In hosted mode, agents run on the hub cluster, otherwise on the spoke cluster
|
|
agentClient = spoke.KubeClient
|
|
agentNamespace = universalAgentNamespace
|
|
if klusterlet.Spec.DeployOption.Mode == operatorapiv1.InstallModeHosted ||
|
|
klusterlet.Spec.DeployOption.Mode == operatorapiv1.InstallModeSingletonHosted {
|
|
agentClient = hub.KubeClient
|
|
agentNamespace = klusterlet.Name
|
|
}
|
|
|
|
deployment, err := agentClient.AppsV1().Deployments(agentNamespace).Get(
|
|
context.TODO(), registrationDeploymentName, metav1.GetOptions{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
initialGeneration = deployment.Generation
|
|
|
|
ginkgo.By("Update klusterlet to use token-based authentication for addons")
|
|
gomega.Eventually(func() error {
|
|
klusterlet, err := spoke.OperatorClient.OperatorV1().Klusterlets().Get(
|
|
context.TODO(), universalKlusterletName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if klusterlet.Spec.RegistrationConfiguration == nil {
|
|
klusterlet.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationConfiguration{}
|
|
}
|
|
|
|
klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver = &operatorapiv1.AddOnRegistrationDriver{
|
|
AuthType: "token",
|
|
Token: &operatorapiv1.TokenConfig{
|
|
ExpirationSeconds: 3600, // 1 hour for testing
|
|
},
|
|
}
|
|
|
|
_, err = spoke.OperatorClient.OperatorV1().Klusterlets().Update(
|
|
context.TODO(), klusterlet, metav1.UpdateOptions{})
|
|
return err
|
|
}).Should(gomega.Succeed())
|
|
|
|
ginkgo.By("Verify klusterlet is updated with token auth configuration")
|
|
gomega.Eventually(func() error {
|
|
klusterlet, err := spoke.OperatorClient.OperatorV1().Klusterlets().Get(
|
|
context.TODO(), universalKlusterletName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if klusterlet.Spec.RegistrationConfiguration == nil ||
|
|
klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver == nil {
|
|
return fmt.Errorf("token auth configuration not set")
|
|
}
|
|
|
|
if klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver.AuthType != "token" {
|
|
return fmt.Errorf("auth type is not token: %s",
|
|
klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver.AuthType)
|
|
}
|
|
|
|
return nil
|
|
}).Should(gomega.Succeed())
|
|
|
|
ginkgo.By("Wait for registration agent deployment to rollout with new token auth configuration")
|
|
gomega.Eventually(func() error {
|
|
deployment, err := agentClient.AppsV1().Deployments(agentNamespace).Get(
|
|
context.TODO(), registrationDeploymentName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Wait for deployment generation to increment (indicates config change was applied)
|
|
if deployment.Generation <= initialGeneration {
|
|
return fmt.Errorf("deployment generation has not incremented yet: current=%d, initial=%d",
|
|
deployment.Generation, initialGeneration)
|
|
}
|
|
|
|
// Ensure the deployment controller has observed the latest spec
|
|
if deployment.Status.ObservedGeneration != deployment.Generation {
|
|
return fmt.Errorf("deployment has not observed latest generation: observed=%d, current=%d",
|
|
deployment.Status.ObservedGeneration, deployment.Generation)
|
|
}
|
|
|
|
// Ensure all replicas have been updated with the new configuration
|
|
if deployment.Status.UpdatedReplicas != deployment.Status.Replicas {
|
|
return fmt.Errorf("deployment has not updated all replicas: updated=%d, total=%d",
|
|
deployment.Status.UpdatedReplicas, deployment.Status.Replicas)
|
|
}
|
|
|
|
// Ensure all updated replicas are ready
|
|
if deployment.Status.ReadyReplicas != deployment.Status.Replicas {
|
|
return fmt.Errorf("deployment not fully ready: ready=%d, total=%d",
|
|
deployment.Status.ReadyReplicas, deployment.Status.Replicas)
|
|
}
|
|
|
|
// Ensure there are no unavailable replicas
|
|
if deployment.Status.UnavailableReplicas > 0 {
|
|
return fmt.Errorf("deployment has unavailable replicas: %d", deployment.Status.UnavailableReplicas)
|
|
}
|
|
|
|
return nil
|
|
}, "2m", "5s").Should(gomega.Succeed())
|
|
|
|
signerSecretNamespace = "signer-secret-token-ns-" + rand.String(6)
|
|
ginkgo.By("Create addon custom sign secret namespace")
|
|
_, err = hub.KubeClient.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: signerSecretNamespace,
|
|
},
|
|
}, metav1.CreateOptions{})
|
|
if err != nil && !errors.IsAlreadyExists(err) {
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
}
|
|
|
|
ginkgo.By("Create addon custom sign secret")
|
|
err = copySignerSecret(context.TODO(), hub.KubeClient, "open-cluster-management-hub",
|
|
"signer-secret", signerSecretNamespace, customSignerSecretName)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
})
|
|
|
|
ginkgo.AfterAll(func() {
|
|
ginkgo.By("Delete addon custom sign secret")
|
|
err := hub.KubeClient.CoreV1().Secrets(signerSecretNamespace).Delete(context.TODO(),
|
|
customSignerSecretName, metav1.DeleteOptions{})
|
|
if err != nil && !errors.IsNotFound(err) {
|
|
ginkgo.Fail(fmt.Sprintf("failed to delete custom signer secret %v/%v: %v",
|
|
signerSecretNamespace, customSignerSecretName, err))
|
|
}
|
|
|
|
ginkgo.By("Delete addon custom sign secret namespace")
|
|
err = hub.KubeClient.CoreV1().Namespaces().Delete(context.TODO(), signerSecretNamespace, metav1.DeleteOptions{})
|
|
if err != nil && !errors.IsNotFound(err) {
|
|
ginkgo.Fail(fmt.Sprintf("failed to delete custom signer secret namespace %v: %v", signerSecretNamespace, err))
|
|
}
|
|
|
|
ginkgo.By("Restore original klusterlet AddOnKubeClientRegistrationDriver configuration")
|
|
gomega.Eventually(func() error {
|
|
klusterlet, err := spoke.OperatorClient.OperatorV1().Klusterlets().Get(
|
|
context.TODO(), universalKlusterletName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if klusterlet.Spec.RegistrationConfiguration == nil {
|
|
klusterlet.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationConfiguration{}
|
|
}
|
|
|
|
klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver = originalAddOnDriver
|
|
|
|
_, err = spoke.OperatorClient.OperatorV1().Klusterlets().Update(
|
|
context.TODO(), klusterlet, metav1.UpdateOptions{})
|
|
return err
|
|
}).Should(gomega.Succeed())
|
|
|
|
})
|
|
|
|
ginkgo.It("Should work with token-based authentication flow", func() {
|
|
var err error
|
|
|
|
ginkgo.By("Step 1: Create addon template resources")
|
|
err = createResourcesFromYamlFiles(context.Background(), hub.DynamicClient, hub.RestMapper, s,
|
|
defaultAddonTemplateReaderManifestsFunc(manifests.AddonManifestFiles, map[string]interface{}{
|
|
"Namespace": universalClusterName,
|
|
"AddonInstallNamespace": addonInstallNamespace,
|
|
"CustomSignerName": customSignerName,
|
|
"AddonManagerNamespace": templateagent.AddonManagerNamespace(),
|
|
"CustomSignerSecretName": customSignerSecretName,
|
|
"CustomSignerSecretNamespace": signerSecretNamespace,
|
|
}),
|
|
templateResources,
|
|
)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
ginkgo.By("Step 2: Create the template addon")
|
|
err = hub.CreateManagedClusterAddOnV1Alpha1(universalClusterName, addOnName, addonInstallNamespace)
|
|
if err != nil {
|
|
gomega.Expect(errors.IsAlreadyExists(err)).To(gomega.BeTrue())
|
|
}
|
|
|
|
ginkgo.By("Step 3: Wait for addon to become available with token authentication")
|
|
gomega.Eventually(func() error {
|
|
return hub.CheckManagedClusterAddOnStatusV1Alpha1(universalClusterName, addOnName)
|
|
}, "5m", "10s").Should(gomega.Succeed())
|
|
|
|
ginkgo.By("Step 4: Verify hub kubeconfig secret is created with token authentication")
|
|
gomega.Eventually(func() error {
|
|
secret, err := agentClient.CoreV1().Secrets(addonInstallNamespace).Get(context.TODO(),
|
|
templateagent.HubKubeconfigSecretName(addOnName), metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify the secret contains token-based kubeconfig
|
|
token, ok := secret.Data["token"]
|
|
if !ok {
|
|
return fmt.Errorf("token not found in secret")
|
|
}
|
|
|
|
// Token should not be empty
|
|
if len(token) == 0 {
|
|
return fmt.Errorf("token is empty")
|
|
}
|
|
|
|
return nil
|
|
}).Should(gomega.Succeed())
|
|
|
|
ginkgo.By("Step 5: Verify custom client cert secret is created")
|
|
gomega.Eventually(func() error {
|
|
_, err := agentClient.CoreV1().Secrets(addonInstallNamespace).Get(context.TODO(),
|
|
templateagent.CustomSignedSecretName(addOnName, customSignerName), metav1.GetOptions{})
|
|
return err
|
|
}).Should(gomega.Succeed())
|
|
|
|
ginkgo.By("Step 6: Test addon functionality - create configmap on hub")
|
|
configmap := &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: fmt.Sprintf("config-token-%s", rand.String(6)),
|
|
Namespace: universalClusterName,
|
|
},
|
|
Data: map[string]string{
|
|
"key1": rand.String(6),
|
|
"key2": rand.String(6),
|
|
},
|
|
}
|
|
|
|
_, err = hub.KubeClient.CoreV1().ConfigMaps(universalClusterName).Create(
|
|
context.Background(), configmap, metav1.CreateOptions{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
ginkgo.By("Step 7: Verify addon copies configmap to spoke using token auth")
|
|
gomega.Eventually(func() error {
|
|
copiedConfig, err := spoke.KubeClient.CoreV1().ConfigMaps(addonInstallNamespace).Get(
|
|
context.Background(), configmap.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !equality.Semantic.DeepEqual(copiedConfig.Data, configmap.Data) {
|
|
return fmt.Errorf("expected configmap is not correct, %v", copiedConfig.Data)
|
|
}
|
|
return nil
|
|
}, "2m", "5s").ShouldNot(gomega.HaveOccurred())
|
|
|
|
ginkgo.By("Step 8: Cleanup - Delete the addon")
|
|
err = hub.AddonClient.AddonV1alpha1().ManagedClusterAddOns(universalClusterName).Delete(
|
|
context.TODO(), addOnName, metav1.DeleteOptions{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
gomega.Eventually(func() error {
|
|
_, err := hub.AddonClient.AddonV1alpha1().ManagedClusterAddOns(universalClusterName).Get(
|
|
context.TODO(), addOnName, metav1.GetOptions{})
|
|
if err == nil {
|
|
return fmt.Errorf("the managedClusterAddon %s should be deleted", addOnName)
|
|
}
|
|
if err != nil && !errors.IsNotFound(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}).ShouldNot(gomega.HaveOccurred())
|
|
|
|
ginkgo.By("Step 9: Delete addon template resources")
|
|
err = deleteResourcesFromYamlFiles(context.Background(), hub.DynamicClient, hub.RestMapper, s,
|
|
defaultAddonTemplateReaderManifestsFunc(manifests.AddonManifestFiles, map[string]interface{}{
|
|
"Namespace": universalClusterName,
|
|
"AddonInstallNamespace": addonInstallNamespace,
|
|
"CustomSignerName": customSignerName,
|
|
"AddonManagerNamespace": templateagent.AddonManagerNamespace(),
|
|
"CustomSignerSecretName": customSignerSecretName,
|
|
"CustomSignerSecretNamespace": signerSecretNamespace,
|
|
}),
|
|
templateResources,
|
|
)
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
ginkgo.By("Step 10: Cleanup CSRs")
|
|
gomega.Eventually(func() error {
|
|
csrs, err := hub.KubeClient.CertificatesV1().CertificateSigningRequests().List(context.TODO(),
|
|
metav1.ListOptions{
|
|
LabelSelector: fmt.Sprintf("%s=%s,%s=%s", addonapiv1alpha1.AddonLabelKey, addOnName,
|
|
clusterv1.ClusterNameLabelKey, universalClusterName),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, csr := range csrs.Items {
|
|
err = hub.KubeClient.CertificatesV1().CertificateSigningRequests().Delete(context.TODO(),
|
|
csr.Name, metav1.DeleteOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}).ShouldNot(gomega.HaveOccurred())
|
|
})
|
|
})
|