mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-08-23 22:26:49 +00:00
* test: add v1beta1 integration tests for addon API Add comprehensive v1beta1 test coverage for addon API following alpha/beta split pattern. Tests for both v1alpha1 and v1beta1 run in parallel with API conversion handled automatically by the test environment. Migrated tests (11 test suites): - addon_manager_install: split into _alpha + beta versions - addon_configs: split into _alpha + beta versions - addon_manager_upgrade: split into _alpha + beta versions - agent_deploy: split into _alpha + beta versions - token_infrastructure: split into _alpha + beta versions - addon_manager_template: renamed to _alpha only (AddOnTemplate API removed in v1beta1) Changes: - suite_test.go: renamed client import to support both API versions - assertion_test.go: added alpha/beta config specs and beta helper functions v1beta1 API compatibility fixes: - Removed deprecated ConfigReferent field from ConfigReference structs - Removed InstallNamespace from ManagedClusterAddOnSpec - Updated RegistrationConfig to use Type field with nested KubeClient/CustomSigner configs - KubeClientDriver moved from top-level Status to RegistrationConfig.KubeClient.Driver - SignerName implicit for kubeClient type, explicit in CustomSigner for customSigner type All integration tests now have complete v1beta1 coverage. E2E tests will be migrated separately. Refs: https://github.com/open-cluster-management-io/ocm/issues/1517 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Signed-off-by: Tesshu Flower <tflower@redhat.com> * test: add ConfigReferent to v1beta1 ConfigSpecHash test expectations The v1beta1 ConfigSpecHash structure includes an inlined ConfigReferent field that specifies the config namespace and name. The controller populates this field in status, so test expectations must include it. Added ConfigReferent to all ConfigSpecHash assertions throughout the addon_manager_upgrade_test.go progressive rollout scenarios: - Initial config deployment (configDefaultName) - After upgrade to test1 config - Progressive rollout start (clusters 0-1 with configUpdateName) - Progressive rollout continuation (clusters 2-4 with configDefaultName) - After rollout completion (configUpdateName with test2) - Mixed state during test3 rollout (DesiredConfig test3, LastAppliedConfig test2) - Final state with test3 config This fixes v1beta1 integration test failures where assertions expected empty ConfigReferent but the controller correctly populated the fields. Signed-off-by: Tesshu Flower <tflower@redhat.com> * test: fix PR review issues in v1beta1 integration tests Address all issues identified in PR #1546 code review: 1. Split assertion helper functions into Alpha/Beta versions - assertClusterManagementAddOnAnnotations -> Alpha/Beta versions - assertClusterManagementAddOnConditions -> Alpha/Beta versions - assertClusterManagementAddOnNoConditions -> Alpha/Beta versions - assertManagedClusterAddOnConditions -> Alpha/Beta versions Original functions were hardcoded to v1alpha1 API but called from both v1alpha1 and v1beta1 tests. Beta versions now use v1beta1 API correctly. Note: assertClusterManagementAddOnAnnotationsBeta is a no-op since AddonLifecycleAnnotationKey was removed in v1beta1 (lifecycle is always managed by addon-manager in v1beta1). 2. Fix inverted JSON comparison in agent_deploy tests - Original bug: compared raw JSON bytes which failed due to whitespace - Tests were passing incorrectly (inverted logic + byte mismatch = false positive) - Fix: unmarshal both JSONs and compare objects semantically - Applied to both agent_deploy_alpha_test.go and agent_deploy_test.go 3. Fix clusterNames accumulation across test cases - Added clusterNames = nil reset in BeforeEach blocks - Prevents cleanup failures from stale cluster references - Applied to both addon_manager_upgrade_alpha_test.go and _test.go 4. Add bounds checking to assertion helpers - Added len(actual.Status.InstallProgressions) checks before indexing - Prevents panic during Eventually polling when status not yet populated - Applied to all 4 condition assertion functions (Alpha/Beta variants) - Returns retryable error instead of panicking, allowing Eventually to continue All 27 integration tests now pass consistently. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Signed-off-by: Tesshu Flower <tflower@redhat.com> * test: fix timing issue in ProgressDeadline assertion Fix flaky test failure in addon rollout ProgressDeadline tests where the timeout would fire ~1 second earlier than expected from the test's perspective. Root cause: The test was capturing the start timestamp AFTER updating the ManifestWork status to False. When the work status changes, the addon controller sets the ManagedClusterAddOn Progressing condition with a LastTransitionTime. The rollout SDK uses that LastTransitionTime (not when the CMA is patched with ProgressDeadline config) as the base for timeout calculation. This meant: 1. Line 365: Work status set to False → Controller sets Progressing=True with LastTransitionTime=T1 2. Line 382: Test captures start=T2 (where T2 = T1 + ~1s) 3. Line 383: CMA patched with ProgressDeadline="5s" 4. Controller calculates timeout as T1 + 5s = T2 + 4s (appears as 4s to test) Fix: Capture start BEFORE the work status update, ensuring the test's timer starts before the controller's timer, so timeouts always appear ≥5s from start. Verified by code review: - pkg/addon/controllers/addonprogressing/controller.go:213 sets Progressing condition - pkg/addon/controllers/addonconfiguration/graph.go:88,91 copies LastTransitionTime - vendor/.../sdk-go/.../rollout.go:487 uses LastTransitionTime for timeout calc Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> 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>
321 lines
12 KiB
Go
321 lines
12 KiB
Go
package integration
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
ginkgo "github.com/onsi/ginkgo/v2"
|
|
"github.com/onsi/gomega"
|
|
certificates "k8s.io/api/certificates/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/util/rand"
|
|
|
|
addonapiv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
|
|
clusterv1 "open-cluster-management.io/api/cluster/v1"
|
|
)
|
|
|
|
var _ = ginkgo.Describe("Token Infrastructure Controller Alpha", func() {
|
|
var managedClusterName, addOnName string
|
|
var err error
|
|
|
|
ginkgo.BeforeEach(func() {
|
|
suffix := rand.String(5)
|
|
managedClusterName = fmt.Sprintf("cluster-%s", suffix)
|
|
addOnName = fmt.Sprintf("addon-%s", suffix)
|
|
|
|
// Create managed cluster
|
|
managedCluster := &clusterv1.ManagedCluster{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: managedClusterName,
|
|
},
|
|
Spec: clusterv1.ManagedClusterSpec{
|
|
HubAcceptsClient: true,
|
|
},
|
|
}
|
|
_, err = hubClusterClient.ClusterV1().ManagedClusters().Create(context.Background(), managedCluster, metav1.CreateOptions{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
// Create cluster namespace
|
|
ns := &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: managedClusterName,
|
|
},
|
|
}
|
|
_, err = hubKubeClient.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
})
|
|
|
|
ginkgo.AfterEach(func() {
|
|
err = hubClusterClient.ClusterV1().ManagedClusters().Delete(context.Background(), managedClusterName, metav1.DeleteOptions{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
err = hubKubeClient.CoreV1().Namespaces().Delete(context.Background(), managedClusterName, metav1.DeleteOptions{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
})
|
|
|
|
assertTokenInfrastructureReady := func(clusterName, addonName string) {
|
|
ginkgo.By("Verify token infrastructure resources are created")
|
|
serviceAccountName := fmt.Sprintf("%s-agent", addonName)
|
|
roleName := fmt.Sprintf("%s-token-role", addonName)
|
|
roleBindingName := fmt.Sprintf("%s-token-role", addonName)
|
|
|
|
// Check ServiceAccount exists with correct labels
|
|
gomega.Eventually(func() error {
|
|
sa, err := hubKubeClient.CoreV1().ServiceAccounts(clusterName).Get(context.Background(), serviceAccountName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if sa.Labels["addon.open-cluster-management.io/token-infrastructure"] != "true" {
|
|
return fmt.Errorf("ServiceAccount missing token-infrastructure label")
|
|
}
|
|
if sa.Labels["addon.open-cluster-management.io/name"] != addonName {
|
|
return fmt.Errorf("ServiceAccount missing addon name label")
|
|
}
|
|
return nil
|
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
|
|
|
// Check Role exists with correct labels
|
|
gomega.Eventually(func() error {
|
|
role, err := hubKubeClient.RbacV1().Roles(clusterName).Get(context.Background(), roleName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if role.Labels["addon.open-cluster-management.io/token-infrastructure"] != "true" {
|
|
return fmt.Errorf("Role missing token-infrastructure label")
|
|
}
|
|
if role.Labels["addon.open-cluster-management.io/name"] != addonName {
|
|
return fmt.Errorf("Role missing addon name label")
|
|
}
|
|
return nil
|
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
|
|
|
// Check RoleBinding exists with correct labels
|
|
gomega.Eventually(func() error {
|
|
rb, err := hubKubeClient.RbacV1().RoleBindings(clusterName).Get(context.Background(), roleBindingName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rb.Labels["addon.open-cluster-management.io/token-infrastructure"] != "true" {
|
|
return fmt.Errorf("RoleBinding missing token-infrastructure label")
|
|
}
|
|
if rb.Labels["addon.open-cluster-management.io/name"] != addonName {
|
|
return fmt.Errorf("RoleBinding missing addon name label")
|
|
}
|
|
return nil
|
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
|
|
|
ginkgo.By("Verify TokenInfrastructureReady condition is set to True")
|
|
gomega.Eventually(func() error {
|
|
addon, err := hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(clusterName).Get(context.Background(), addonName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cond := meta.FindStatusCondition(addon.Status.Conditions, "TokenInfrastructureReady")
|
|
if cond == nil {
|
|
return fmt.Errorf("TokenInfrastructureReady condition not found")
|
|
}
|
|
if cond.Status != metav1.ConditionTrue {
|
|
return fmt.Errorf("TokenInfrastructureReady condition is not True: %s - %s", cond.Reason, cond.Message)
|
|
}
|
|
return nil
|
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
|
}
|
|
|
|
assertTokenInfrastructureCleanedUp := func(clusterName, addonName string) {
|
|
ginkgo.By("Verify token infrastructure resources are deleted")
|
|
serviceAccountName := fmt.Sprintf("%s-agent", addonName)
|
|
roleName := fmt.Sprintf("%s-token-role", addonName)
|
|
roleBindingName := fmt.Sprintf("%s-token-role", addonName)
|
|
|
|
// Check ServiceAccount is deleted
|
|
gomega.Eventually(func() bool {
|
|
_, err := hubKubeClient.CoreV1().ServiceAccounts(clusterName).Get(context.Background(), serviceAccountName, metav1.GetOptions{})
|
|
return errors.IsNotFound(err)
|
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
|
|
|
// Check Role is deleted
|
|
gomega.Eventually(func() bool {
|
|
_, err := hubKubeClient.RbacV1().Roles(clusterName).Get(context.Background(), roleName, metav1.GetOptions{})
|
|
return errors.IsNotFound(err)
|
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
|
|
|
// Check RoleBinding is deleted
|
|
gomega.Eventually(func() bool {
|
|
_, err := hubKubeClient.RbacV1().RoleBindings(clusterName).Get(context.Background(), roleBindingName, metav1.GetOptions{})
|
|
return errors.IsNotFound(err)
|
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
|
}
|
|
|
|
ginkgo.It("should create token infrastructure when addon uses token driver", func() {
|
|
ginkgo.By("Create ManagedClusterAddOn")
|
|
addOn := &addonapiv1alpha1.ManagedClusterAddOn{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: addOnName,
|
|
Namespace: managedClusterName,
|
|
},
|
|
Spec: addonapiv1alpha1.ManagedClusterAddOnSpec{
|
|
InstallNamespace: addOnName,
|
|
},
|
|
}
|
|
_, err = hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Create(context.Background(), addOn, metav1.CreateOptions{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
ginkgo.By("Update addon status with kubeClient registration and token driver")
|
|
gomega.Eventually(func() error {
|
|
addon, err := hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.Background(), addOnName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
addon.Status.Registrations = []addonapiv1alpha1.RegistrationConfig{
|
|
{
|
|
SignerName: certificates.KubeAPIServerClientSignerName,
|
|
},
|
|
}
|
|
addon.Status.KubeClientDriver = "token"
|
|
_, err = hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.Background(), addon, metav1.UpdateOptions{})
|
|
return err
|
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
|
|
|
assertTokenInfrastructureReady(managedClusterName, addOnName)
|
|
})
|
|
|
|
ginkgo.It("should cleanup token infrastructure when addon switches from token to CSR driver", func() {
|
|
ginkgo.By("Create ManagedClusterAddOn with token driver")
|
|
addOn := &addonapiv1alpha1.ManagedClusterAddOn{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: addOnName,
|
|
Namespace: managedClusterName,
|
|
},
|
|
Spec: addonapiv1alpha1.ManagedClusterAddOnSpec{
|
|
InstallNamespace: addOnName,
|
|
},
|
|
}
|
|
_, err = hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Create(context.Background(), addOn, metav1.CreateOptions{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
gomega.Eventually(func() error {
|
|
addon, err := hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.Background(), addOnName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
addon.Status.Registrations = []addonapiv1alpha1.RegistrationConfig{
|
|
{
|
|
SignerName: certificates.KubeAPIServerClientSignerName,
|
|
},
|
|
}
|
|
addon.Status.KubeClientDriver = "token"
|
|
_, err = hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.Background(), addon, metav1.UpdateOptions{})
|
|
return err
|
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
|
|
|
assertTokenInfrastructureReady(managedClusterName, addOnName)
|
|
|
|
ginkgo.By("Switch addon to CSR driver")
|
|
gomega.Eventually(func() error {
|
|
addon, err := hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.Background(), addOnName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update kubeClientDriver to switch to CSR driver
|
|
addon.Status.KubeClientDriver = "csr"
|
|
_, err = hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.Background(), addon, metav1.UpdateOptions{})
|
|
return err
|
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
|
|
|
assertTokenInfrastructureCleanedUp(managedClusterName, addOnName)
|
|
|
|
ginkgo.By("Verify TokenInfrastructureReady condition is removed")
|
|
gomega.Eventually(func() bool {
|
|
addon, err := hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.Background(), addOnName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
cond := meta.FindStatusCondition(addon.Status.Conditions, "TokenInfrastructureReady")
|
|
return cond == nil
|
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
|
})
|
|
|
|
ginkgo.It("should cleanup token infrastructure when addon is deleted", func() {
|
|
ginkgo.By("Create ManagedClusterAddOn with token driver")
|
|
addOn := &addonapiv1alpha1.ManagedClusterAddOn{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: addOnName,
|
|
Namespace: managedClusterName,
|
|
},
|
|
Spec: addonapiv1alpha1.ManagedClusterAddOnSpec{
|
|
InstallNamespace: addOnName,
|
|
},
|
|
}
|
|
_, err = hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Create(context.Background(), addOn, metav1.CreateOptions{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
gomega.Eventually(func() error {
|
|
addon, err := hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.Background(), addOnName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
addon.Status.Registrations = []addonapiv1alpha1.RegistrationConfig{
|
|
{
|
|
SignerName: certificates.KubeAPIServerClientSignerName,
|
|
},
|
|
}
|
|
addon.Status.KubeClientDriver = "token"
|
|
_, err = hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.Background(), addon, metav1.UpdateOptions{})
|
|
return err
|
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
|
|
|
assertTokenInfrastructureReady(managedClusterName, addOnName)
|
|
|
|
ginkgo.By("Delete the addon")
|
|
err = hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Delete(context.Background(), addOnName, metav1.DeleteOptions{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
assertTokenInfrastructureCleanedUp(managedClusterName, addOnName)
|
|
})
|
|
|
|
ginkgo.It("should handle addon with multiple registrations where only one is token-based", func() {
|
|
ginkgo.By("Create ManagedClusterAddOn with multiple registrations including token driver")
|
|
addOn := &addonapiv1alpha1.ManagedClusterAddOn{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: addOnName,
|
|
Namespace: managedClusterName,
|
|
},
|
|
Spec: addonapiv1alpha1.ManagedClusterAddOnSpec{
|
|
InstallNamespace: addOnName,
|
|
},
|
|
}
|
|
_, err = hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Create(context.Background(), addOn, metav1.CreateOptions{})
|
|
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
|
|
|
gomega.Eventually(func() error {
|
|
addon, err := hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.Background(), addOnName, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
addon.Status.Registrations = []addonapiv1alpha1.RegistrationConfig{
|
|
{
|
|
SignerName: certificates.KubeAPIServerClientSignerName,
|
|
},
|
|
{
|
|
SignerName: "example.com/custom-signer",
|
|
},
|
|
}
|
|
addon.Status.KubeClientDriver = "token"
|
|
_, err = hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.Background(), addon, metav1.UpdateOptions{})
|
|
return err
|
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
|
|
|
assertTokenInfrastructureReady(managedClusterName, addOnName)
|
|
})
|
|
})
|