Skip to content

Commit 1e3ac51

Browse files
mnenciaNiccoloFei
andauthored
test(e2e): add operator pull secret to shared ServiceAccounts (cloudnative-pg#10303)
The shared ServiceAccount E2E tests were failing in environments where the operator image requires authentication, because the shared SA had no imagePullSecrets for the bootstrap-controller init container. Replicate what a real user would do by copying the operator pull secret to the test namespace and attaching it to the shared SA. The pull secret name is read from the operator deployment's PULL_SECRET_NAME env var to support non-default configurations. Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com> Signed-off-by: Niccolò Fei <niccolo.fei@enterprisedb.com> Co-authored-by: Niccolò Fei <niccolo.fei@enterprisedb.com>
1 parent b7b9ee5 commit 1e3ac51

File tree

3 files changed

+108
-16
lines changed

3 files changed

+108
-16
lines changed

tests/e2e/cluster_shared_service_account_test.go

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import (
2727
"github.com/cloudnative-pg/cloudnative-pg/tests"
2828
"github.com/cloudnative-pg/cloudnative-pg/tests/utils/clusterutils"
2929
"github.com/cloudnative-pg/cloudnative-pg/tests/utils/objects"
30-
"github.com/cloudnative-pg/cloudnative-pg/tests/utils/pods"
30+
"github.com/cloudnative-pg/cloudnative-pg/tests/utils/operator"
31+
"github.com/cloudnative-pg/cloudnative-pg/tests/utils/secrets"
3132

3233
. "github.com/onsi/ginkgo/v2"
3334
. "github.com/onsi/gomega"
@@ -53,27 +54,25 @@ var _ = Describe("Shared ServiceAccount", Label(tests.LabelBasic), func() {
5354
namespace, err := env.CreateUniqueTestNamespace(env.Ctx, env.Client, namespacePrefix)
5455
Expect(err).ToNot(HaveOccurred())
5556

56-
By("creating a shared ServiceAccount", func() {
57+
By("creating a shared ServiceAccount with operator pull secrets", func() {
5758
CreateResourceFromFile(namespace, sharedSAFile)
59+
operatorDeployment, err := operator.GetDeployment(env.Ctx, env.Client)
60+
Expect(err).ToNot(HaveOccurred())
61+
Expect(secrets.CopyOperatorPullSecretToServiceAccount(
62+
env.Ctx, env.Client, operatorDeployment, namespace, sharedSAName,
63+
)).To(Succeed())
5864
})
5965

6066
By("creating cluster using shared ServiceAccount", func() {
6167
AssertCreateCluster(namespace, cluster1Name, cluster1File, env)
6268
})
6369

6470
By("verifying cluster pods use the shared ServiceAccount", func() {
65-
podList, err := pods.List(env.Ctx, env.Client, namespace)
71+
podList, err := clusterutils.ListPods(env.Ctx, env.Client, namespace, cluster1Name)
6672
Expect(err).ToNot(HaveOccurred())
73+
Expect(podList.Items).ToNot(BeEmpty())
6774

68-
cluster1Pods := []corev1.Pod{}
6975
for _, pod := range podList.Items {
70-
if pod.Labels["cnpg.io/cluster"] == cluster1Name {
71-
cluster1Pods = append(cluster1Pods, pod)
72-
}
73-
}
74-
Expect(cluster1Pods).ToNot(BeEmpty())
75-
76-
for _, pod := range cluster1Pods {
7776
Expect(pod.Spec.ServiceAccountName).To(Equal(sharedSAName),
7877
"Pod %s should use shared ServiceAccount %s", pod.Name, sharedSAName)
7978
}

tests/e2e/pooler_shared_service_account_test.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import (
2828
apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
2929
"github.com/cloudnative-pg/cloudnative-pg/pkg/utils"
3030
"github.com/cloudnative-pg/cloudnative-pg/tests"
31+
"github.com/cloudnative-pg/cloudnative-pg/tests/utils/deployments"
32+
"github.com/cloudnative-pg/cloudnative-pg/tests/utils/operator"
33+
"github.com/cloudnative-pg/cloudnative-pg/tests/utils/secrets"
3134

3235
. "github.com/onsi/ginkgo/v2"
3336
. "github.com/onsi/gomega"
@@ -59,20 +62,27 @@ var _ = Describe("Pooler Shared ServiceAccount", Label(tests.LabelBasic), func()
5962
AssertCreateCluster(namespace, clusterName, clusterFile, env)
6063
})
6164

62-
By("creating a shared ServiceAccount", func() {
65+
By("creating a shared ServiceAccount with operator pull secrets", func() {
6366
CreateResourceFromFile(namespace, sharedSAFile)
67+
operatorDeployment, err := operator.GetDeployment(env.Ctx, env.Client)
68+
Expect(err).ToNot(HaveOccurred())
69+
Expect(secrets.CopyOperatorPullSecretToServiceAccount(
70+
env.Ctx, env.Client, operatorDeployment, namespace, sharedSAName,
71+
)).To(Succeed())
6472
})
6573

6674
By("creating pooler using shared ServiceAccount", func() {
6775
CreateResourceFromFile(namespace, pooler1File)
6876
})
6977

7078
By("waiting for pooler deployment to be ready", func() {
71-
Eventually(func() error {
79+
Eventually(func(g Gomega) {
7280
var deployment appsv1.Deployment
73-
return env.Client.Get(env.Ctx,
74-
client.ObjectKey{Namespace: namespace, Name: pooler1Name},
75-
&deployment)
81+
err := env.Client.Get(env.Ctx, client.ObjectKey{Namespace: namespace, Name: pooler1Name}, &deployment)
82+
g.Expect(err).ToNot(HaveOccurred())
83+
g.Expect(deployments.IsReady(deployment)).To(BeTrue(),
84+
"Pooler deployment %s/%s is not ready", namespace, pooler1Name,
85+
)
7686
}, 300).Should(Succeed())
7787
})
7888

tests/utils/secrets/secrets.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ import (
2424
"context"
2525
"fmt"
2626

27+
appsv1 "k8s.io/api/apps/v1"
2728
corev1 "k8s.io/api/core/v1"
2829
apierrors "k8s.io/apimachinery/pkg/api/errors"
2930
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3031
"k8s.io/apimachinery/pkg/types"
3132
"sigs.k8s.io/controller-runtime/pkg/client"
3233

3334
apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
35+
"github.com/cloudnative-pg/cloudnative-pg/internal/configuration"
3436
"github.com/cloudnative-pg/cloudnative-pg/pkg/certs"
3537
"github.com/cloudnative-pg/cloudnative-pg/tests/utils/clusterutils"
3638
"github.com/cloudnative-pg/cloudnative-pg/tests/utils/objects"
@@ -111,6 +113,87 @@ func GetCredentials(
111113
return username, password, nil
112114
}
113115

116+
// CopyOperatorPullSecretToServiceAccount copies the operator pull secret from
117+
// the operator namespace to the target namespace and adds it as an
118+
// imagePullSecret on the specified ServiceAccount. This is needed for shared
119+
// ServiceAccounts that are not managed by the operator, so they can pull the
120+
// operator image used by the bootstrap-controller init container.
121+
// If no operator pull secret exists, this is a no-op.
122+
func CopyOperatorPullSecretToServiceAccount(
123+
ctx context.Context,
124+
crudClient client.Client,
125+
operatorDeployment appsv1.Deployment,
126+
targetNamespace, serviceAccountName string,
127+
) error {
128+
pullSecretName := getOperatorPullSecretName(operatorDeployment)
129+
operatorNamespace := operatorDeployment.Namespace
130+
131+
// Get the operator pull secret
132+
var operatorSecret corev1.Secret
133+
err := crudClient.Get(ctx, client.ObjectKey{
134+
Name: pullSecretName,
135+
Namespace: operatorNamespace,
136+
}, &operatorSecret)
137+
if apierrors.IsNotFound(err) {
138+
return nil
139+
}
140+
if err != nil {
141+
return fmt.Errorf("while getting operator pull secret: %w", err)
142+
}
143+
144+
// Copy the secret to the target namespace
145+
targetSecret := &corev1.Secret{
146+
ObjectMeta: metav1.ObjectMeta{
147+
Name: pullSecretName,
148+
Namespace: targetNamespace,
149+
},
150+
Data: operatorSecret.Data,
151+
Type: operatorSecret.Type,
152+
}
153+
if err := crudClient.Create(ctx, targetSecret); err != nil && !apierrors.IsAlreadyExists(err) {
154+
return fmt.Errorf("while creating pull secret in target namespace: %w", err)
155+
}
156+
157+
// Add the pull secret to the ServiceAccount
158+
var sa corev1.ServiceAccount
159+
if err := crudClient.Get(ctx, client.ObjectKey{
160+
Name: serviceAccountName,
161+
Namespace: targetNamespace,
162+
}, &sa); err != nil {
163+
return fmt.Errorf("while getting service account: %w", err)
164+
}
165+
166+
for _, ref := range sa.ImagePullSecrets {
167+
if ref.Name == pullSecretName {
168+
return nil
169+
}
170+
}
171+
172+
original := sa.DeepCopy()
173+
sa.ImagePullSecrets = append(sa.ImagePullSecrets, corev1.LocalObjectReference{
174+
Name: pullSecretName,
175+
})
176+
if err := crudClient.Patch(ctx, &sa, client.MergeFrom(original)); err != nil {
177+
return fmt.Errorf("while patching service account with pull secret: %w", err)
178+
}
179+
180+
return nil
181+
}
182+
183+
// getOperatorPullSecretName reads the PULL_SECRET_NAME env var from the
184+
// operator deployment, falling back to the default name.
185+
// NOTE: this only inspects literal env values, not valueFrom references.
186+
func getOperatorPullSecretName(deployment appsv1.Deployment) string {
187+
for _, container := range deployment.Spec.Template.Spec.Containers {
188+
for _, envVar := range container.Env {
189+
if envVar.Name == "PULL_SECRET_NAME" && envVar.Value != "" {
190+
return envVar.Value
191+
}
192+
}
193+
}
194+
return configuration.DefaultOperatorPullSecretName
195+
}
196+
114197
// CreateObjectStorageSecret generates an Opaque Secret with a given ID and Key
115198
func CreateObjectStorageSecret(
116199
ctx context.Context,

0 commit comments

Comments
 (0)