Files
dc1278427f 🌱 add v1beta1 integration tests for addon API - preserve v1alpha1 tests (#1546)
* 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>
2026-06-02 15:11:02 +00:00

219 lines
8.6 KiB
Go

package integration
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/rand"
"open-cluster-management.io/addon-framework/pkg/addonmanager/constants"
"open-cluster-management.io/addon-framework/pkg/agent"
addonapiv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
clusterv1 "open-cluster-management.io/api/cluster/v1"
workapiv1 "open-cluster-management.io/api/work/v1"
)
const (
deploymentJson = `{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "nginx-deployment",
"namespace": "default"
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app": "nginx"
}
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": [
{
"image": "nginx:1.14.2",
"name": "nginx",
"ports": [
{
"containerPort": 80,
"protocol": "TCP"
}
]
}
]
}
}
}
}`
)
var _ = ginkgo.Describe("Agent deploy Alpha", func() {
var managedClusterName string
var err error
var manifestWorkName string
ginkgo.BeforeEach(func() {
suffix := rand.String(5)
managedClusterName = fmt.Sprintf("managedcluster-%s", suffix)
manifestWorkName = fmt.Sprintf("%s-0", constants.DeployWorkNamePrefix(testAddonImpl.name))
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())
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: managedClusterName}}
_, err = hubKubeClient.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
cma := newClusterManagementAddonAlpha(testAddonImpl.name)
_, err = hubAddonClient.AddonV1alpha1().ClusterManagementAddOns().Create(context.Background(),
cma, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
assertClusterManagementAddOnAnnotationsAlpha(testAddonImpl.name)
})
ginkgo.AfterEach(func() {
err = hubKubeClient.CoreV1().Namespaces().Delete(context.Background(), managedClusterName, metav1.DeleteOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
err = hubClusterClient.ClusterV1().ManagedClusters().Delete(context.Background(), managedClusterName, metav1.DeleteOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
err = hubAddonClient.AddonV1alpha1().ClusterManagementAddOns().Delete(context.Background(),
testAddonImpl.name, metav1.DeleteOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
})
ginkgo.It("Should deploy agent when cma is managed by addon-manager successfully", func() {
obj := &unstructured.Unstructured{}
err := obj.UnmarshalJSON([]byte(deploymentJson))
gomega.Expect(err).ToNot(gomega.HaveOccurred())
testAddonImpl.manifests[managedClusterName] = []runtime.Object{obj}
testAddonImpl.prober = &agent.HealthProber{
Type: agent.HealthProberTypeWork,
}
// Create ManagedClusterAddOn
addon := &addonapiv1alpha1.ManagedClusterAddOn{
ObjectMeta: metav1.ObjectMeta{
Name: testAddonImpl.name,
},
Spec: addonapiv1alpha1.ManagedClusterAddOnSpec{
InstallNamespace: "default",
},
}
_, err = hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Create(context.Background(), addon, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Eventually(func() error {
work, err := hubWorkClient.WorkV1().ManifestWorks(managedClusterName).Get(context.Background(), manifestWorkName, metav1.GetOptions{})
if err != nil {
return err
}
if len(work.Spec.Workload.Manifests) != 1 {
return fmt.Errorf("unexpected number of work manifests")
}
// Unmarshal both JSONs and compare semantically (not byte-for-byte)
var expected, actual map[string]interface{}
if err := json.Unmarshal([]byte(deploymentJson), &expected); err != nil {
return fmt.Errorf("failed to unmarshal expected JSON: %v", err)
}
if err := json.Unmarshal(work.Spec.Workload.Manifests[0].Raw, &actual); err != nil {
return fmt.Errorf("failed to unmarshal actual JSON: %v", err)
}
if !apiequality.Semantic.DeepEqual(expected, actual) {
return fmt.Errorf("expected manifest does not match actual, got %v", string(work.Spec.Workload.Manifests[0].Raw))
}
return nil
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
// Update work status to trigger addon status
work, err := hubWorkClient.WorkV1().ManifestWorks(managedClusterName).Get(context.Background(), manifestWorkName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
meta.SetStatusCondition(
&work.Status.Conditions,
metav1.Condition{Type: workapiv1.WorkApplied, Status: metav1.ConditionTrue, Reason: "WorkApplied", ObservedGeneration: work.Generation})
_, err = hubWorkClient.WorkV1().ManifestWorks(managedClusterName).UpdateStatus(context.Background(), work, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Eventually(func() error {
addon, err := hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.Background(), testAddonImpl.name, metav1.GetOptions{})
if err != nil {
return err
}
if !meta.IsStatusConditionTrue(addon.Status.Conditions, addonapiv1alpha1.ManagedClusterAddOnManifestApplied) {
return fmt.Errorf("unexpected addon applied condition, %v", addon.Status.Conditions)
}
if !meta.IsStatusConditionTrue(addon.Status.Conditions, addonapiv1alpha1.ManagedClusterAddOnConditionProgressing) {
return fmt.Errorf("unexpected addon progressing condition, %v", addon.Status.Conditions)
}
return nil
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
// update work to available so addon becomes available
work, err = hubWorkClient.WorkV1().ManifestWorks(managedClusterName).Get(context.Background(), manifestWorkName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
meta.SetStatusCondition(
&work.Status.Conditions,
metav1.Condition{Type: workapiv1.WorkAvailable, Status: metav1.ConditionTrue, Reason: "WorkAvailable", ObservedGeneration: work.Generation})
_, err = hubWorkClient.WorkV1().ManifestWorks(managedClusterName).UpdateStatus(context.Background(), work, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Eventually(func() error {
addon, err := hubAddonClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.Background(), testAddonImpl.name, metav1.GetOptions{})
if err != nil {
return err
}
if !meta.IsStatusConditionTrue(addon.Status.Conditions, addonapiv1alpha1.ManagedClusterAddOnConditionAvailable) {
return fmt.Errorf("unexpected addon available condition, %v", addon.Status.Conditions)
}
if !meta.IsStatusConditionFalse(addon.Status.Conditions, addonapiv1alpha1.ManagedClusterAddOnConditionProgressing) {
return fmt.Errorf("unexpected addon progressing condition, %v", addon.Status.Conditions)
}
return nil
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
// do nothing if cluster is deleting and addon is not deleted
cluster, err := hubClusterClient.ClusterV1().ManagedClusters().Get(context.Background(), managedClusterName, metav1.GetOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
cluster.SetFinalizers([]string{clusterv1.ManagedClusterFinalizer})
_, err = hubClusterClient.ClusterV1().ManagedClusters().Update(context.Background(), cluster, metav1.UpdateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
err = hubClusterClient.ClusterV1().ManagedClusters().Delete(context.Background(), managedClusterName, metav1.DeleteOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
time.Sleep(5 * time.Second) // wait 5 seconds to sync
gomega.Eventually(func() error {
_, err = hubWorkClient.WorkV1().ManifestWorks(managedClusterName).Get(context.Background(), manifestWorkName, metav1.GetOptions{})
return err
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
})
})