Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,64 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() {
}).Should(BeTrue())

})

It("verifies that the deprecated spec.logformat field is honoured for applicationSet and notifications", func() {
By("creating a fresh test namespace")
ns, cleanupFunc = fixture.CreateRandomE2ETestNamespaceWithCleanupFunc()

By("creating ArgoCD CR using the deprecated lowercase logformat field on applicationSet and notifications")

argoCD := &argov1beta1api.ArgoCD{
ObjectMeta: metav1.ObjectMeta{Name: "argocd", Namespace: ns.Name},
Spec: argov1beta1api.ArgoCDSpec{
Server: argov1beta1api.ArgoCDServerSpec{
Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true},
},
ApplicationSet: &argov1beta1api.ArgoCDApplicationSet{
//nolint:staticcheck // intentionally using deprecated field to verify backward compatibility in e2e
Logformat: "json",
},
Notifications: argov1beta1api.ArgoCDNotifications{
Enabled: true,
//nolint:staticcheck // intentionally using deprecated field to verify backward compatibility in e2e
Logformat: "json",
},
},
}
Expect(k8sClient.Create(ctx, argoCD)).To(Succeed())

By("waiting for the ArgoCD instance to become fully available")

Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable())

deploymentCommandContains := func(deplName, flag, value string) bool {
depl := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: deplName, Namespace: ns.Name},
}
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(depl), depl); err != nil {
GinkgoWriter.Println("error fetching deployment", deplName, ":", err)
return false
}
if len(depl.Spec.Template.Spec.Containers) == 0 {
GinkgoWriter.Println("deployment", deplName, "has no containers yet")
return false
}
cmdStr := strings.Join(depl.Spec.Template.Spec.Containers[0].Command, " ")
GinkgoWriter.Println(deplName, "command:", cmdStr)

return strings.Contains(cmdStr, flag+" "+value)
}
By("verifying argocd-applicationset-controller Deployment has --logformat json from deprecated field")

Eventually(func() bool {
return deploymentCommandContains("argocd-applicationset-controller", "--logformat", "json")
}, "2m", "5s").Should(BeTrue())
By("verifying argocd-notifications-controller Deployment has --logformat json from deprecated field")

Eventually(func() bool {
return deploymentCommandContains("argocd-notifications-controller", "--logformat", "json")
}, "2m", "5s").Should(BeTrue())

})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture"
argocdFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/argocd"
k8sFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/k8s"
"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"
Expand Down Expand Up @@ -94,6 +95,43 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() {
}
Expect(match).To(BeTrue(), "StatefulSet should have expected ARGOCD_CONTROLLER_REPLICAS")

By("ensuring algorithm can be set")
argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) {
ac.Spec.Controller.Sharding = argov1beta1api.ArgoCDApplicationControllerShardSpec{
Enabled: true,
Replicas: 3,
DistributionAlgorithm: "round-robin",
}
})

By("checking if ARGOCD_CONTROLLER_SHARDING_ALGORITHM env var is set in the app controller StatefulSet")
Eventually(statefulSet).Should(k8sFixture.ExistByName())
Eventually(statefulSet, "60s", "5s").Should(statefulset.HaveContainerWithEnvVar("ARGOCD_CONTROLLER_SHARDING_ALGORITHM", "round-robin", 0), "Statefulset should have expected ARGOCD_CONTROLLER_SHARDING_ALGORITHM to be round-robin")

By("unset algorithm and ensure that it is not set")
argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) {
ac.Spec.Controller.Sharding = argov1beta1api.ArgoCDApplicationControllerShardSpec{
Enabled: true,
Replicas: 3,
}
})

By("checking if ARGOCD_CONTROLLER_SHARDING_ALGORITHM env var is not set in app controller StatefulSet")
Eventually(statefulSet).Should(k8sFixture.ExistByName())
Eventually(func() bool {
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(statefulSet), statefulSet); err != nil {
return false
}
if len(statefulSet.Spec.Template.Spec.Containers) == 0 {
return false
}
for _, env := range statefulSet.Spec.Template.Spec.Containers[0].Env {
if env.Name == "ARGOCD_CONTROLLER_SHARDING_ALGORITHM" {
return false
}
}
return true
}, "60s", "5s").Should(BeTrue(), "StatefulSet should have no env variable named ARGOCD_CONTROLLER_SHARDING_ALGORITHM")
By("disabling sharding")
argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) {
ac.Spec.Controller.Sharding = argov1beta1api.ArgoCDApplicationControllerShardSpec{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,44 @@ package parallel

import (
"context"
"strings"
"time"

argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1"
"github.com/argoproj-labs/argocd-operator/common"
. "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"
k8sFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/k8s"
secretFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/secret"
fixtureUtils "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/utils"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// newArgoCDForDexOpenShiftOAuthE2E returns the ArgoCD CR.
func newArgoCDForDexOpenShiftOAuthE2E(namespace string) *argov1beta1api.ArgoCD {
return &argov1beta1api.ArgoCD{
ObjectMeta: metav1.ObjectMeta{Name: "example-argocd", Namespace: namespace},
Spec: argov1beta1api.ArgoCDSpec{
SSO: &argov1beta1api.ArgoCDSSOSpec{
Provider: argov1beta1api.SSOProviderTypeDex,
Dex: &argov1beta1api.ArgoCDDexSpec{
OpenShiftOAuth: true,
},
},
Server: argov1beta1api.ArgoCDServerSpec{
Route: argov1beta1api.ArgoCDRouteSpec{
Enabled: true,
},
},
},
}
}

var _ = Describe("GitOps Operator Parallel E2E Tests", func() {

Context("1-095_validate_dex_clientsecret", func() {
Expand All @@ -47,69 +72,195 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() {
ctx = context.Background()
})

It("verifies that Dex serviceaccount token secret is not leaked, and is correctly set in Argo CD argocd-secret Secret", func() {
It("verifies that the Dex client secret is sourced from a short-lived TokenRequest token and is correctly set in argocd-secret", func() {

By("creating simple Argo CD instance with Dex and Openshift OAuth enabled")
ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc()
defer cleanupFunc()

argoCD := &argov1beta1api.ArgoCD{
ObjectMeta: metav1.ObjectMeta{Name: "example-argocd", Namespace: ns.Name},
Spec: argov1beta1api.ArgoCDSpec{
SSO: &argov1beta1api.ArgoCDSSOSpec{
Provider: argov1beta1api.SSOProviderTypeDex,
Dex: &argov1beta1api.ArgoCDDexSpec{
OpenShiftOAuth: true,
},
},
Server: argov1beta1api.ArgoCDServerSpec{
Route: argov1beta1api.ArgoCDRouteSpec{
Enabled: true,
},
},
},
}
argoCD := newArgoCDForDexOpenShiftOAuthE2E(ns.Name)
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())

serviceAccount := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-argocd-dex-server", Namespace: ns.Name}}
dexSAName := "example-argocd-argocd-dex-server"
serviceAccount := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: dexSAName, Namespace: ns.Name}}
Eventually(serviceAccount).Should(k8sFixture.ExistByName())

argocdCM := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "argocd-cm", Namespace: ns.Name},
}
Eventually(argocdCM).Should(k8sFixture.ExistByName())
By("verifying no additional non-expiring kubernetes.io/service-account-token Secrets exist for the Dex SA beyond platform-created ones (OCP <4.16 creates one for image registry)")
Consistently(func() bool {
secretList := &corev1.SecretList{}
if err := k8sClient.List(ctx, secretList, client.InNamespace(ns.Name)); err != nil {
return false
}

tokenCount := 0
for _, s := range secretList.Items {
if s.Type == corev1.SecretTypeServiceAccountToken &&
strings.HasPrefix(s.Name, dexSAName+"-token-") &&
s.Annotations[corev1.ServiceAccountNameKey] == dexSAName {
tokenCount++
}
}

// Allow max 1 token (platform-created on OCP 4.14/4.15), but operator shouldn't create more
// On OCP 4.16+, tokenCount will be 0 as no automatic tokens are created
return tokenCount <= 1
}, "20s", "4s").Should(BeTrue(), "operator should not create additional legacy kubernetes.io/service-account-token Secrets beyond platform-created ones")

By("verifying argocd-cm ConfigMap is not leaking oidc dex client secret")
dexConfig := argocdCM.Data["dex.config"]
argocdCM := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "argocd-cm", Namespace: ns.Name}}
Eventually(argocdCM).Should(k8sFixture.ExistByName())

Expect(dexConfig).To(ContainSubstring("clientSecret: $oidc.dex.clientSecret"), "'$oidc.dex.clientSecret' should be set. Any other value implies that the client secret is exposed via ConfigMap")
Eventually(func() bool {
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(argocdCM), argocdCM); err != nil {
return false
}
return strings.Contains(argocdCM.Data["dex.config"], "clientSecret: $oidc.dex.clientSecret")
}, "2m", "5s").Should(BeTrue(), "'$oidc.dex.clientSecret' should be set. Any other value implies that the client secret is exposed via ConfigMap")

By("validating that the Dex Client Secret was copied from dex serviceaccount token secret in to argocd-secret, by the operator")
By("verifying the Dex SA has no non-expiring kubernetes.io/service-account-token Secrets in its .secrets list")
dexSANoLegacyTokenRefs := func() bool {
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(serviceAccount), serviceAccount); err != nil {
return false
}
for _, ref := range serviceAccount.Secrets {
if strings.Contains(ref.Name, "dex-server-token") {
GinkgoWriter.Println("Dex SA still has legacy token Secret reference:", ref.Name)
return false
}
}
return true
}
Eventually(dexSANoLegacyTokenRefs, "2m", "5s").Should(BeTrue(), "Dex SA .secrets must not reference any legacy non-expiring token Secrets")
By("verifying that absence of legacy token Secret references in the Dex SA .secrets list persists")
Consistently(dexSANoLegacyTokenRefs, "20s", "4s").Should(BeTrue(), "Dex SA .secrets must keep no legacy non-expiring token Secret references")

// The operator now creates an Opaque secret with a deterministic name for the Dex token
// (via TokenRequest API) instead of using auto-generated kubernetes.io/service-account-token secrets.
// The secret name follows the pattern: <argocd-name>-<dex-sa-name>-token
dexTokenSecretName := "example-argocd-argocd-dex-server-token" // #nosec G101 -- This is a Kubernetes secret name, not a credential
By("verifying the dedicated short-lived Dex token Secret was created by the operator")
tokenSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-argocd-dex-server-token", Namespace: ns.Name}}
Eventually(tokenSecret, "2m", "5s").Should(k8sFixture.ExistByName())
Eventually(tokenSecret).Should(secretFixture.HaveNonEmptyKeyValue("token"))
Eventually(tokenSecret).Should(secretFixture.HaveNonEmptyKeyValue("expiry"))

// Extract the clientSecret from the Dex token secret
dexTokenSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: dexTokenSecretName, Namespace: ns.Name}}
Eventually(dexTokenSecret, "30s", "2s").Should(k8sFixture.ExistByName())
tokenFromDexSecret := dexTokenSecret.Data["token"]
Expect(tokenFromDexSecret).ToNot(BeEmpty())
// Verify the secret also contains an expiry field
Expect(dexTokenSecret.Data["expiry"]).ToNot(BeEmpty())
By("verifying the token expiry is a valid RFC3339 timestamp in the future")
Eventually(func() bool {
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(tokenSecret), tokenSecret); err != nil {
return false
}
expiry, err := time.Parse(time.RFC3339, string(tokenSecret.Data["expiry"]))
if err != nil {
GinkgoWriter.Println("expiry is not valid RFC3339:", string(tokenSecret.Data["expiry"]), err)
return false
}
GinkgoWriter.Println("token expiry:", expiry.UTC())
return time.Until(expiry) > 0
}, "2m", "5s").Should(BeTrue(), "Dex token 'expiry' must be a valid RFC3339 timestamp in the future")

// actualClientSecret is the value of the secret in argocd-secret where argocd-operator should copy the secret from
By("validating that the Dex client secret in argocd-secret matches the token in the dedicated token Secret")
argocdSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "argocd-secret", Namespace: ns.Name}}
Eventually(argocdSecret).Should(k8sFixture.ExistByName())
Eventually(argocdSecret).Should(secretFixture.HaveNonEmptyKeyValue("oidc.dex.clientSecret"))

actualClientSecret := argocdSecret.Data["oidc.dex.clientSecret"]
Eventually(func() bool {
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(tokenSecret), tokenSecret); err != nil {
return false
}
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(argocdSecret), argocdSecret); err != nil {
return false
}
return string(tokenSecret.Data["token"]) == string(argocdSecret.Data["oidc.dex.clientSecret"])
}, "2m", "5s").Should(BeTrue(), "Dex client secret in argocd-secret must match the token in the dedicated Dex token Secret")
})

Expect(string(actualClientSecret)).To(Equal(string(tokenFromDexSecret)), "Dex Client Secret for OIDC is not valid")
It("verifies the operator deletes legacy non-expiring Dex kubernetes.io/service-account-token Secrets and drops them from the Dex SA", func() {

By("creating simple Argo CD instance with Dex and Openshift OAuth enabled")
ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc()
defer cleanupFunc()

argoCD := newArgoCDForDexOpenShiftOAuthE2E(ns.Name)
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())

dexSAName := "example-argocd-argocd-dex-server"
dexSA := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: dexSAName, Namespace: ns.Name}}
Eventually(dexSA).Should(k8sFixture.ExistByName())

legacyName := dexSAName + "-token-e2elegacy"
By("creating a legacy non-expiring kubernetes.io/service-account-token Secret for the Dex SA (operator-tracked label required for cleanup list)")
legacySecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: legacyName,
Namespace: ns.Name,
Labels: map[string]string{
common.ArgoCDTrackedByOperatorLabel: common.ArgoCDAppName,
},
Annotations: map[string]string{
corev1.ServiceAccountNameKey: dexSAName,
},
},
Type: corev1.SecretTypeServiceAccountToken,
}
Expect(k8sClient.Create(ctx, legacySecret)).To(Succeed())

By("adding the legacy Secret to the Dex SA .secrets list to mimic stale controller state")
Eventually(func() bool {
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dexSA), dexSA); err != nil {
GinkgoWriter.Printf("get Dex SA: %v\n", err)
return false
}
for _, ref := range dexSA.Secrets {
if ref.Name == legacyName {
return true
}
}
dexSA.Secrets = append(dexSA.Secrets, corev1.ObjectReference{Name: legacyName})
if err := k8sClient.Update(ctx, dexSA); err != nil {
GinkgoWriter.Printf("update Dex SA with legacy secret ref: %v\n", err)
return false
}
return true
}, "2m", "3s").Should(BeTrue(), "Dex SA should list the synthetic legacy token Secret reference")

By("triggering reconciliation so the operator runs legacy Dex token Secret cleanup (creating the Secret does not enqueue the ArgoCD reconcile)")
argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) {
if ac.Annotations == nil {
ac.Annotations = make(map[string]string)
}
ac.Annotations["test.argocd.argoproj.io/trigger-legacy-dex-token-reconcile"] = time.Now().Format(time.RFC3339Nano)
})

By("waiting for the operator to delete the legacy Secret")
legacySecretGone := func() bool {
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(legacySecret), legacySecret)
return apierrors.IsNotFound(err)
}
Eventually(legacySecretGone, "2m", "5s").Should(BeTrue(), "legacy kubernetes.io/service-account-token Secret must be deleted")
By("verifying the legacy Secret stays deleted")
Consistently(legacySecretGone, "20s", "4s").Should(BeTrue(), "legacy kubernetes.io/service-account-token Secret must not reappear")

By("waiting for the Dex SA to no longer reference legacy dex-server-token Secrets")
dexSANoLegacyTokenRefs := func() bool {
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dexSA), dexSA); err != nil {
return false
}
for _, ref := range dexSA.Secrets {
if strings.Contains(ref.Name, "dex-server-token") {
GinkgoWriter.Println("Dex SA still has legacy token Secret reference:", ref.Name)
return false
}
}
return true
}
Eventually(dexSANoLegacyTokenRefs, "2m", "5s").Should(BeTrue(), "Dex SA .secrets must not reference legacy non-expiring token Secrets")
By("verifying that absence of legacy token Secret references in the Dex SA .secrets list persists")
Consistently(dexSANoLegacyTokenRefs, "20s", "4s").Should(BeTrue(), "Dex SA .secrets must keep no legacy non-expiring token Secret references")

By("verifying the Argo CD instance stays healthy after legacy cleanup")
Eventually(argoCD, "2m", "5s").Should(argocdFixture.BeAvailable())
})

})
Expand Down
Loading
Loading