Skip to content

Commit 8b4bb09

Browse files
jvanzviccuad
andauthored
feat: foward image pull secrets (#1583)
* feat: foward image pull secrets Adds a new controller CLI flag to allow used to define a list of secret that must be set in the policy server deployments to allow the container image to be downloaded from a private registry. Signed-off-by: José Guilherme Vanz <jguilhermevanz@suse.com> Assisted-by: Github Copilot * feat(charts): use flag to define image pull secrets Updates the kubewarden-controller Helm chart to use the new controller CLI flag that allow users to define a list of secrets with the data to allowing container image download from private registries. Signed-off-by: José Guilherme Vanz <jguilhermevanz@suse.com> Assisted-by: Github Copilot * docs: Restore CRD, helm values docstrings Signed-off-by: Víctor Cuadrado Juan <vcuadradojuan@suse.de> --------- Signed-off-by: José Guilherme Vanz <jguilhermevanz@suse.com> Signed-off-by: Víctor Cuadrado Juan <vcuadradojuan@suse.de> Co-authored-by: Víctor Cuadrado Juan <vcuadradojuan@suse.de>
1 parent 7ea28fd commit 8b4bb09

10 files changed

Lines changed: 182 additions & 1 deletion

File tree

api/policies/v1/policyserver_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ type PolicyServerSpec struct {
6767
// +optional
6868
ServiceAccountName string `json:"serviceAccountName,omitempty"`
6969

70-
// Name of ImagePullSecret secret in the same namespace, used for pulling
70+
// Name of ImagePullSecret secret in the same namespace. used for pulling
7171
// policies from repositories.
7272
// +optional
7373
ImagePullSecret string `json:"imagePullSecret,omitempty"`

charts/kubewarden-controller/templates/_helpers.tpl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,24 @@ Create the name of the service account to use for kubewarden-controller
112112
{{- end -}}
113113
{{- end -}}
114114

115+
{{/*
116+
Build a comma-separated list of Secret names from .Values.imagePullSecrets,
117+
for use with the controller --image-pull-secrets flag. Handles both string
118+
entries and {name: ...} objects. Returns an empty string when no secrets
119+
are configured.
120+
*/}}
121+
{{- define "policyServerImagePullSecretNames" -}}
122+
{{- $names := list -}}
123+
{{- range .Values.imagePullSecrets -}}
124+
{{- if kindIs "string" . -}}
125+
{{- $names = append $names . -}}
126+
{{- else -}}
127+
{{- $names = append $names .name -}}
128+
{{- end -}}
129+
{{- end -}}
130+
{{- join "," $names -}}
131+
{{- end -}}
132+
115133
{{- define "audit-scanner.command" -}}
116134
{{- $parallelNamespaces := .Values.auditScanner.parallelNamespaces | int -}}
117135
{{- $parallelResources := .Values.auditScanner.parallelResources | int -}}

charts/kubewarden-controller/templates/deployment.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ spec:
5858
{{- if .Values.mTLS.enable }}
5959
- --client-ca-configmap-name={{ .Values.mTLS.configMapName }}
6060
{{- end }}
61+
{{- $imagePullSecretNames := include "policyServerImagePullSecretNames" . }}
62+
{{- if $imagePullSecretNames }}
63+
- --image-pull-secrets={{ $imagePullSecretNames }}
64+
{{- end }}
6165
{{- if or .Values.telemetry.metrics .Values.telemetry.tracing }}
6266
{{- if eq .Values.telemetry.mode "sidecar" }}
6367
- --enable-otel-sidecar
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
suite: image-pull-secrets flag
2+
templates:
3+
- deployment.yaml
4+
tests:
5+
- it: "should not include --image-pull-secrets when imagePullSecrets is empty (default)"
6+
asserts:
7+
- notContains:
8+
path: spec.template.spec.containers[0].args
9+
content: "--image-pull-secrets"
10+
any: true
11+
12+
- it: "should include --image-pull-secrets when imagePullSecrets contains a string entry"
13+
set:
14+
imagePullSecrets:
15+
- my-registry-secret
16+
asserts:
17+
- contains:
18+
path: spec.template.spec.containers[0].args
19+
content: "--image-pull-secrets=my-registry-secret"
20+
21+
- it: "should include --image-pull-secrets when imagePullSecrets contains an object entry"
22+
set:
23+
imagePullSecrets:
24+
- name: my-registry-secret
25+
asserts:
26+
- contains:
27+
path: spec.template.spec.containers[0].args
28+
content: "--image-pull-secrets=my-registry-secret"
29+
30+
- it: "should include multiple imagePullSecrets as a comma-separated list"
31+
set:
32+
imagePullSecrets:
33+
- secret-one
34+
- secret-two
35+
asserts:
36+
- contains:
37+
path: spec.template.spec.containers[0].args
38+
content: "--image-pull-secrets=secret-one,secret-two"

cmd/controller/main.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"fmt"
2424
"os"
2525
"path/filepath"
26+
"strings"
2627

2728
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
2829
// to ensure that exec-entrypoint and run can make use of them.
@@ -72,6 +73,7 @@ type Configuration struct {
7273
ClientCAConfigMapName string
7374
FeatureGateAdmissionWebhookMatchConditions bool
7475
WebhookServiceName string
76+
ImagePullSecrets []corev1.LocalObjectReference
7577
}
7678

7779
func init() {
@@ -93,6 +95,7 @@ func main() {
9395
var enableOtelSidecar bool
9496
var openTelemetryClientCertificateSecret string
9597
var openTelemetryCertificateSecret string
98+
var imagePullSecretsFlag string
9699

97100
flag.StringVar(&mgrOpts.MetricsAddr, "metrics-bind-address", ":8088", "The address the metric endpoint binds to.")
98101
flag.StringVar(&mgrOpts.ProbeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
@@ -120,11 +123,16 @@ func main() {
120123
false,
121124
"Always accept admission reviews targeting the deployments-namespace.")
122125
flag.StringVar(&config.ClientCAConfigMapName, "client-ca-configmap-name", "", "The name of the ConfigMap containing the client CA certificate. If provided, mTLS will be enabled.")
126+
flag.StringVar(&imagePullSecretsFlag,
127+
"image-pull-secrets",
128+
"",
129+
"Comma-separated list of Secret names to use as imagePullSecrets on every policy-server Deployment. The secrets must exist in the deployments namespace.")
123130

124131
opts := zap.Options{}
125132
opts.BindFlags(flag.CommandLine)
126133
flag.Parse()
127134
mgrOpts.EnableMutualTLS = config.ClientCAConfigMapName != ""
135+
config.ImagePullSecrets = parseImagePullSecrets(imagePullSecretsFlag)
128136
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
129137

130138
if enableMetrics {
@@ -290,6 +298,7 @@ func setupReconcilers(mgr ctrl.Manager,
290298
AlwaysAcceptAdmissionReviewsInDeploymentsNamespace: config.AlwaysAcceptAdmissionReviewsOnDeploymentsNamespace,
291299
TelemetryConfiguration: otelConfiguration,
292300
ClientCAConfigMapName: config.ClientCAConfigMapName,
301+
ImagePullSecrets: config.ImagePullSecrets,
293302
}).SetupWithManager(mgr); err != nil {
294303
return errors.Join(errors.New("unable to create PolicyServer controller"), err)
295304
}
@@ -365,3 +374,20 @@ func setupWebhooks(mgr ctrl.Manager, deploymentsNamespace string) error {
365374
}
366375
return nil
367376
}
377+
378+
// parseImagePullSecrets converts a comma-separated list of secret names into a
379+
// slice of LocalObjectReferences. Empty names are ignored. An empty or blank
380+
// input string returns nil.
381+
func parseImagePullSecrets(s string) []corev1.LocalObjectReference {
382+
if s == "" {
383+
return nil
384+
}
385+
var refs []corev1.LocalObjectReference
386+
for _, name := range strings.Split(s, ",") {
387+
name = strings.TrimSpace(name)
388+
if name != "" {
389+
refs = append(refs, corev1.LocalObjectReference{Name: name})
390+
}
391+
}
392+
return refs
393+
}

internal/controller/policyserver_controller.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"github.com/go-logr/logr"
2525

26+
corev1 "k8s.io/api/core/v1"
2627
apierrors "k8s.io/apimachinery/pkg/api/errors"
2728
apimeta "k8s.io/apimachinery/pkg/api/meta"
2829
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -62,6 +63,10 @@ type PolicyServerReconciler struct {
6263
DeploymentsNamespace string
6364
AlwaysAcceptAdmissionReviewsInDeploymentsNamespace bool
6465
ClientCAConfigMapName string
66+
// ImagePullSecrets is the list of image pull secrets to add to every
67+
// policy-server Deployment, sourced from the controller's own
68+
// --image-pull-secrets flag.
69+
ImagePullSecrets []corev1.LocalObjectReference
6570
}
6671

6772
// TelemetryConfiguration is a struct that contains the configuration for the

internal/controller/policyserver_controller_deployment.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ func (r *PolicyServerReconciler) updatePolicyServerDeployment(ctx context.Contex
136136
configMapVersion,
137137
templateAnnotations,
138138
podSecurityContext,
139+
r.ImagePullSecrets,
139140
)
140141
r.adaptDeploymentForMetricsAndTracingConfiguration(policyServerDeployment, templateAnnotations)
141142
r.adaptDeploymentSettingsForPolicyServer(policyServerDeployment, policyServer)
@@ -414,6 +415,7 @@ func buildPolicyServerDeploymentSpec(
414415
configMapVersion string,
415416
templateAnnotations map[string]string,
416417
podSecurityContext *corev1.PodSecurityContext,
418+
imagePullSecrets []corev1.LocalObjectReference,
417419
) appsv1.DeploymentSpec {
418420
templateLabels := map[string]string{
419421
//nolint:staticcheck // this label will remove soon when policy lifecycle is revisited
@@ -444,6 +446,7 @@ func buildPolicyServerDeploymentSpec(
444446
Spec: corev1.PodSpec{
445447
SecurityContext: podSecurityContext,
446448
Containers: []corev1.Container{admissionContainer},
449+
ImagePullSecrets: imagePullSecrets,
447450
ServiceAccountName: policyServer.Spec.ServiceAccountName,
448451
Tolerations: policyServer.Spec.Tolerations,
449452
Affinity: &policyServer.Spec.Affinity,

internal/controller/policyserver_controller_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
corev1 "k8s.io/api/core/v1"
3232
k8spoliciesv1 "k8s.io/api/policy/v1"
3333
"k8s.io/apimachinery/pkg/api/resource"
34+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3435
"k8s.io/apimachinery/pkg/types"
3536
"k8s.io/apimachinery/pkg/util/intstr"
3637
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -1303,6 +1304,90 @@ var _ = Describe("PolicyServer controller", func() {
13031304
})
13041305
})
13051306

1307+
When("a PolicyServer has spec.imagePullSecret set", func() {
1308+
It("should mount the Docker config volume but NOT add it to Deployment imagePullSecrets", func() {
1309+
secretName := newName("pull-secret")
1310+
pullSecret := &corev1.Secret{
1311+
ObjectMeta: metav1.ObjectMeta{
1312+
Name: secretName,
1313+
Namespace: deploymentsNamespace,
1314+
},
1315+
Type: corev1.SecretTypeDockerConfigJson,
1316+
Data: map[string][]byte{
1317+
corev1.DockerConfigJsonKey: []byte(`{"auths":{}}`),
1318+
},
1319+
}
1320+
Expect(k8sClient.Create(ctx, pullSecret)).To(haveSucceededOrAlreadyExisted())
1321+
1322+
policyServer := policiesv1.NewPolicyServerFactory().WithName(policyServerName).WithImagePullSecret(secretName).Build()
1323+
createPolicyServerAndWaitForItsService(ctx, policyServer)
1324+
1325+
Eventually(func() *appsv1.Deployment {
1326+
deployment, err := getTestPolicyServerDeployment(ctx, policyServerName)
1327+
if err != nil {
1328+
return nil
1329+
}
1330+
return deployment
1331+
}, timeout, pollInterval).Should(PointTo(MatchFields(IgnoreExtras, Fields{
1332+
"Spec": MatchFields(IgnoreExtras, Fields{
1333+
"Template": MatchFields(IgnoreExtras, Fields{
1334+
"Spec": MatchFields(IgnoreExtras, Fields{
1335+
"ImagePullSecrets": Not(ContainElement(MatchFields(IgnoreExtras, Fields{
1336+
"Name": Equal(secretName),
1337+
}))),
1338+
"Volumes": ContainElement(MatchFields(IgnoreExtras, Fields{
1339+
"Name": Equal(imagePullSecretVolumeName),
1340+
"VolumeSource": MatchFields(IgnoreExtras, Fields{
1341+
"Secret": PointTo(MatchFields(IgnoreExtras, Fields{
1342+
"SecretName": Equal(secretName),
1343+
})),
1344+
}),
1345+
})),
1346+
"Containers": ContainElement(MatchFields(IgnoreExtras, Fields{
1347+
"VolumeMounts": ContainElement(MatchFields(IgnoreExtras, Fields{
1348+
"Name": Equal(imagePullSecretVolumeName),
1349+
"MountPath": Equal(dockerConfigJSONPolicyServerPath),
1350+
})),
1351+
"Env": ContainElement(MatchFields(IgnoreExtras, Fields{
1352+
"Name": Equal("KUBEWARDEN_DOCKER_CONFIG_JSON_PATH"),
1353+
"Value": Equal(dockerConfigJSONPolicyServerPath),
1354+
})),
1355+
})),
1356+
}),
1357+
}),
1358+
}),
1359+
})))
1360+
})
1361+
})
1362+
1363+
When("a PolicyServer has no spec.imagePullSecret but reconciler has ImagePullSecrets", func() {
1364+
It("should set reconciler secrets in Deployment imagePullSecrets without mounting a Docker config volume", func() {
1365+
policyServer := policiesv1.NewPolicyServerFactory().WithName(policyServerName).Build()
1366+
createPolicyServerAndWaitForItsService(ctx, policyServer)
1367+
1368+
Eventually(func() *appsv1.Deployment {
1369+
deployment, err := getTestPolicyServerDeployment(ctx, policyServerName)
1370+
if err != nil {
1371+
return nil
1372+
}
1373+
return deployment
1374+
}, timeout, pollInterval).Should(PointTo(MatchFields(IgnoreExtras, Fields{
1375+
"Spec": MatchFields(IgnoreExtras, Fields{
1376+
"Template": MatchFields(IgnoreExtras, Fields{
1377+
"Spec": MatchFields(IgnoreExtras, Fields{
1378+
"ImagePullSecrets": ContainElement(MatchFields(IgnoreExtras, Fields{
1379+
"Name": Equal(reconcilerImagePullSecret),
1380+
})),
1381+
"Volumes": Not(ContainElement(MatchFields(IgnoreExtras, Fields{
1382+
"Name": Equal(imagePullSecretVolumeName),
1383+
}))),
1384+
}),
1385+
}),
1386+
}),
1387+
})))
1388+
})
1389+
})
1390+
13061391
When("deleting a PolicyServer", func() {
13071392
BeforeEach(func() {
13081393
createPolicyServerAndWaitForItsService(ctx, policiesv1.NewPolicyServerFactory().WithName(policyServerName).Build())

internal/controller/suite_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ var _ = SynchronizedBeforeSuite(func() []byte {
126126
Scheme: k8sManager.GetScheme(),
127127
DeploymentsNamespace: deploymentsNamespace,
128128
ClientCAConfigMapName: clientCAConfigMapName,
129+
ImagePullSecrets: []corev1.LocalObjectReference{{Name: reconcilerImagePullSecret}},
129130
}).SetupWithManager(k8sManager)
130131
Expect(err).ToNot(HaveOccurred())
131132

internal/controller/utils_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const (
4242
integrationTestsFinalizer = "kubewarden.io/integration-tests-safety-net-finalizer"
4343
clientCAConfigMapName = "client-ca"
4444
fakeSigstoreTrustConfig = `{"trusted_root": {"version": "test"}}`
45+
reconcilerImagePullSecret = "reconciler-pull-secret"
4546
)
4647

4748
func getTestClusterAdmissionPolicy(ctx context.Context, name string) (*policiesv1.ClusterAdmissionPolicy, error) {

0 commit comments

Comments
 (0)