diff --git a/.golangci.yml b/.golangci.yml index 6b08446bb..44a3ceee7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -13,6 +13,13 @@ linters: - third_party$ - builtin$ - examples$ + settings: + staticcheck: + # https://staticcheck.dev/docs/configuration/options/#dot_import_whitelist + dot-import-whitelist: + # Ginkgo testsuites + - github.com/onsi/gomega + - github.com/onsi/ginkgo/v2 formatters: exclusions: diff --git a/go.mod b/go.mod index fc2508bfc..8008a1118 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.6 require ( github.com/argoproj-labs/argo-rollouts-manager v0.0.6-0.20250731075119-a100fc1d88b8 - github.com/argoproj-labs/argocd-operator v0.16.0-rc1.0.20251210081855-96c4956c5ce8 + github.com/argoproj-labs/argocd-operator v0.16.1-0.20260104093113-f9da3a7a4a0b github.com/argoproj/argo-cd/v3 v3.1.5 github.com/argoproj/gitops-engine v0.7.1-0.20250905160054-e48120133eec github.com/go-logr/logr v1.4.3 diff --git a/go.sum b/go.sum index ef7260367..661ce534a 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/argoproj-labs/argo-rollouts-manager v0.0.6-0.20250731075119-a100fc1d88b8 h1:6+eo7BKrNkSIhQ1nnyCUloSNrGzghlb8r8e7GokoeBo= github.com/argoproj-labs/argo-rollouts-manager v0.0.6-0.20250731075119-a100fc1d88b8/go.mod h1:yTwzKUV79YyI764hkXdVojGYBA9yKJk3qXx5mRuQ2Xc= -github.com/argoproj-labs/argocd-operator v0.16.0-rc1.0.20251210081855-96c4956c5ce8 h1:r6AJ7fKOEvCevYdtv6Q4HKeqyYW4pGlqZJzDyE6TZcA= -github.com/argoproj-labs/argocd-operator v0.16.0-rc1.0.20251210081855-96c4956c5ce8/go.mod h1:NCFt9E3K/eXfjfuXGQXLe+zKQCeRCaZv7ZsbbXFPpOw= +github.com/argoproj-labs/argocd-operator v0.16.1-0.20260104093113-f9da3a7a4a0b h1:S8X2Qoo9qq4OKL0r3yZYxx8pVIJKoXD/vdptn/QskUw= +github.com/argoproj-labs/argocd-operator v0.16.1-0.20260104093113-f9da3a7a4a0b/go.mod h1:NCFt9E3K/eXfjfuXGQXLe+zKQCeRCaZv7ZsbbXFPpOw= github.com/argoproj/argo-cd/v3 v3.1.5 h1:dm1SY5CaILDIQIQINA4H6uJrXpExyif2Yz5915g91kQ= github.com/argoproj/argo-cd/v3 v3.1.5/go.mod h1:ZHb/LOz/hr88VWMJiVTd8DGYL7MheHCAT8S6DgYOBFo= github.com/argoproj/gitops-engine v0.7.1-0.20250905160054-e48120133eec h1:rNAwbRQFvRIuW/e2bU+B10mlzghYXsnwZedYeA7Drz4= diff --git a/test/openshift/e2e/ginkgo/fixture/statefulset/fixture.go b/test/openshift/e2e/ginkgo/fixture/statefulset/fixture.go index 21ca42152..4b2b31dee 100644 --- a/test/openshift/e2e/ginkgo/fixture/statefulset/fixture.go +++ b/test/openshift/e2e/ginkgo/fixture/statefulset/fixture.go @@ -4,6 +4,7 @@ import ( "context" "reflect" "strings" + "time" //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." . "github.com/onsi/ginkgo/v2" //nolint:all @@ -38,6 +39,15 @@ func Update(obj *appsv1.StatefulSet, modify func(*appsv1.StatefulSet)) { Expect(err).ToNot(HaveOccurred()) } +func Restart(obj *appsv1.StatefulSet) { + Update(obj, func(ss *appsv1.StatefulSet) { + if ss.Spec.Template.Annotations == nil { + ss.Spec.Template.Annotations = make(map[string]string) + } + ss.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + }) +} + func HaveReplicas(replicas int) matcher.GomegaMatcher { return fetchStatefulSet(func(ss *appsv1.StatefulSet) bool { GinkgoWriter.Println("StatefulSet HaveReplicas:", "expected: ", replicas, "actual: ", ss.Status.Replicas) @@ -258,6 +268,35 @@ func HaveContainerWithEnvVar(envKey string, envValue string, containerIndex int) }) } +// HaveContainerWithEnvVarFromConfigMap checks if a container has an env var that references a ConfigMap key +func HaveContainerWithEnvVarFromConfigMap(envKey string, configMapName string, configMapKey string, containerIndex int) matcher.GomegaMatcher { + return fetchStatefulSet(func(ss *appsv1.StatefulSet) bool { + + containers := ss.Spec.Template.Spec.Containers + + if len(containers) <= containerIndex { + GinkgoWriter.Println("current container slice has length", len(containers), "index is", containerIndex) + return false + } + + container := containers[containerIndex] + + for _, env := range container.Env { + if env.Name == envKey { + if env.ValueFrom != nil && env.ValueFrom.ConfigMapKeyRef != nil { + ref := env.ValueFrom.ConfigMapKeyRef + GinkgoWriter.Println("HaveContainerWithEnvVarFromConfigMap - Key:", envKey, + "Expected ConfigMap:", configMapName, "Key:", configMapKey, + "Actual ConfigMap:", ref.Name, "Key:", ref.Key) + return ref.Name == configMapName && ref.Key == configMapKey + } + } + } + + return false + }) +} + // This is intentionally NOT exported, for now. Create another function in this file/package that calls this function, and export that. func fetchStatefulSet(f func(*appsv1.StatefulSet) bool) matcher.GomegaMatcher { diff --git a/test/openshift/e2e/ginkgo/parallel/1-122_validate_argocd_reconciliation_timeout.go b/test/openshift/e2e/ginkgo/parallel/1-122_validate_argocd_reconciliation_timeout.go new file mode 100644 index 000000000..7d16112cf --- /dev/null +++ b/test/openshift/e2e/ginkgo/parallel/1-122_validate_argocd_reconciliation_timeout.go @@ -0,0 +1,222 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parallel + +import ( + "context" + "time" + + argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture" + argocdFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/argocd" + configmapFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/configmap" + k8sFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/k8s" + statefulsetFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/statefulset" + fixtureUtils "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/utils" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("GitOps Operator Parallel E2E Tests", func() { + + Context("1-122_validate_argocd_reconciliation_timeout environment variable", func() { + + var ( + k8sClient client.Client + ctx context.Context + ns *corev1.Namespace + cleanupFunc func() + ) + + BeforeEach(func() { + fixture.EnsureParallelCleanSlate() + k8sClient, _ = fixtureUtils.GetE2ETestKubeClient() + ctx = context.Background() + }) + + AfterEach(func() { + fixture.OutputDebugOnFail(ns) + + if cleanupFunc != nil { + cleanupFunc() + } + }) + + It("should set ARGOCD_RECONCILIATION_TIMEOUT with appSync value", func() { + + By("creating simple namespace-scoped Argo CD instance") + ns, cleanupFunc = fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "argocd", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Controller: argov1beta1api.ArgoCDApplicationControllerSpec{ + AppSync: &metav1.Duration{Duration: time.Minute * 10}, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + ss := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-application-controller", + Namespace: ns.Name, + }, + } + Eventually(ss).Should(k8sFixture.ExistByName()) + Eventually(ss).Should(statefulsetFixture.HaveReplicas(1)) + Eventually(ss).Should(statefulsetFixture.HaveReadyReplicas(1)) + + By("verifying env var is added to argocd-application-controller, and that other env vars are still present") + Eventually(ss).Should(statefulsetFixture.HaveContainerWithEnvVar("ARGOCD_RECONCILIATION_TIMEOUT", "600s", 0)) + + Expect(len(ss.Spec.Template.Spec.Containers[0].Env)).To(BeNumerically(">", 1)) + + By("Updating appSync to 5 minutes") + argoCD.Spec.Controller.AppSync = &metav1.Duration{Duration: time.Minute * 5} + Expect(k8sClient.Update(ctx, argoCD)).To(Succeed()) + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + Eventually(ss).Should(statefulsetFixture.HaveContainerWithEnvVar("ARGOCD_RECONCILIATION_TIMEOUT", "300s", 0)) + + }) + + It("should set environment variable ARGOCD_RECONCILIATION_TIMEOUT with timeout.reconciliation value in configmap", func() { + + By("creating simple namespace-scoped Argo CD instance") + ns, cleanupFunc = fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "argocd", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + ExtraConfig: map[string]string{"timeout.reconciliation": "10m"}, + }, + } + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("Fetching ConfigMap and verifying timeout.reconciliation value") + argocdCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "argocd-cm", Namespace: ns.Name}, + } + Eventually(argocdCM).Should(configmapFixture.HaveStringDataKeyValue("timeout.reconciliation", "10m")) + + By("waiting for StatefulSet to be ready") + ss := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-application-controller", + Namespace: ns.Name, + }, + } + Eventually(ss).Should(k8sFixture.ExistByName()) + Eventually(ss).Should(statefulsetFixture.HaveReplicas(1)) + Eventually(ss).Should(statefulsetFixture.HaveReadyReplicas(1)) + + By("verifying env var is added to example-argocd-application-controller, and that other env vars are still present") + Eventually(ss).Should(statefulsetFixture.HaveContainerWithEnvVarFromConfigMap( + "ARGOCD_RECONCILIATION_TIMEOUT", + "argocd-cm", + "timeout.reconciliation", + 0, + )) + + By("Updating timeout.reconciliation to 5 minutes and waiting for ArgoCD CR to be reconciled and the instance to be ready") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.ExtraConfig["timeout.reconciliation"] = "5m" + }) + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + Eventually(argocdCM).Should(configmapFixture.HaveStringDataKeyValue("timeout.reconciliation", "5m")) + + By("waiting for StatefulSet to be ready after restart") + statefulsetFixture.Restart(ss) + Eventually(ss, "3m", "5s").Should(statefulsetFixture.HaveReadyReplicas(1)) + ss = &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-application-controller", + Namespace: ns.Name, + }, + } + Eventually(ss).Should(statefulsetFixture.HaveContainerWithEnvVarFromConfigMap( + "ARGOCD_RECONCILIATION_TIMEOUT", + "argocd-cm", + "timeout.reconciliation", + 0, + )) + }) + + It("Validate the precedence of the appSync value over the timeout.reconciliation value in configmap", func() { + ns, cleanupFunc = fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + + By("creating ArgoCD CR with appSync value and timeout.reconciliation value in configmap") + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "argocd", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Controller: argov1beta1api.ArgoCDApplicationControllerSpec{ + AppSync: &metav1.Duration{Duration: time.Minute * 10}, + }, + ExtraConfig: map[string]string{"timeout.reconciliation": "15m"}, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + ss := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-application-controller", + Namespace: ns.Name, + }, + } + Eventually(ss).Should(k8sFixture.ExistByName()) + Eventually(ss).Should(statefulsetFixture.HaveReadyReplicas(1)) + + By("verifying env var is added to argocd-application-controller and the appSync value is used") + Eventually(ss).Should(statefulsetFixture.HaveContainerWithEnvVar("ARGOCD_RECONCILIATION_TIMEOUT", "600s", 0)) + + By("Removing appSync value and waiting for ArgoCD CR to be reconciled and the instance to be ready") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Controller.AppSync = nil + }) + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + statefulsetFixture.Restart(ss) + Eventually(ss, "3m", "5s").Should(statefulsetFixture.HaveReadyReplicas(1)) + By("Fetching ConfigMap and verifying timeout.reconciliation value") + argocdCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "argocd-cm", Namespace: ns.Name}, + } + Eventually(argocdCM).Should(configmapFixture.HaveStringDataKeyValue("timeout.reconciliation", "15m")) + By("verifying env var is added to example-argocd-application-controller and the timeout.reconciliation value is used") + Eventually(ss).Should(statefulsetFixture.HaveContainerWithEnvVarFromConfigMap( + "ARGOCD_RECONCILIATION_TIMEOUT", + "argocd-cm", + "timeout.reconciliation", + 0, + )) + + }) + }) +})