mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-08-20 12:46:59 +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>
219 lines
8.6 KiB
Go
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())
|
|
})
|
|
|
|
})
|