Skip to content

Commit c604380

Browse files
authored
feat: Copy ImagePullSecrets from ProviderConfig to ManagedControlPlane (#45)
* feat: add ImagePullSecrets to ProviderConfig, use Juggler to reconcile them on MCP, add to values for helm * fix: pass a list of imagePullSecret names to helm values instead of a map, as expected by the chart template for service account. Adjust tests accordingly. * fix: use pointer receiver * fix: reorder func params and align param names On-behalf-of: Radek Schekalla (SAP) <[email protected]> Signed-off-by: Radek Schekalla (SAP) <[email protected]>
1 parent c9d667c commit c604380

File tree

10 files changed

+446
-39
lines changed

10 files changed

+446
-39
lines changed

api/crds/manifests/crossplane.services.openmcp.cloud_providerconfigs.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,24 @@ spec:
9595
description: ImageMapping holds the information about exchangable
9696
image locations in the Helm chart.
9797
type: object
98+
imagePullSecrets:
99+
description: Image pull secrets for Crossplane pods
100+
items:
101+
description: LocalObjectReference is a reference to an object in
102+
the same namespace as the resource referencing it.
103+
properties:
104+
name:
105+
default: ""
106+
description: |-
107+
Name of the referent.
108+
This field is effectively required, but due to backwards compatibility is
109+
allowed to be empty. Instances of this type with an empty value here are
110+
almost certainly wrong.
111+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
112+
type: string
113+
type: object
114+
x-kubernetes-map-type: atomic
115+
type: array
98116
required:
99117
- availableProviders
100118
- chart

api/v1alpha1/providerconfig_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ type ProviderConfigSpec struct {
6464
// AvailableProviders holds the list of providers that can be configured with the Service Provider Crossplane.
6565
// +kubebuilder:validation:Required
6666
AvailableProviders []AvailableCrossplaneProvider `json:"availableProviders"`
67+
68+
// Image pull secrets for Crossplane pods
69+
// +kubebuilder:validation:Optional
70+
ImagePullSecrets []commonapi.LocalObjectReference `json:"imagePullSecrets,omitempty"`
6771
}
6872

6973
// ProviderConfigStatus defines the observed state of ProviderConfig.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/samples/v1alpha1_providerconfig.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ spec:
1313
- name: provider-kubernetes
1414
package: xpkg.upbound.io/upbound/provider-kubernetes
1515
versions:
16-
- v0.16.0
16+
- v0.16.0
17+
imagePullSecrets:
18+
- name: registry-secret

internal/controller/crossplane_controller.go

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ import (
2020
"context"
2121
"errors"
2222
"fmt"
23+
"os"
2324
"time"
2425

2526
"github.com/go-logr/logr"
2627
"github.com/openmcp-project/controller-utils/pkg/clusters"
2728
"github.com/openmcp-project/controller-utils/pkg/controller/smartrequeue"
29+
openmcpconsts "github.com/openmcp-project/openmcp-operator/api/constants"
2830
corev1 "k8s.io/api/core/v1"
2931
rbac "k8s.io/api/rbac/v1"
3032
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -100,8 +102,14 @@ func (r *CrossplaneReconciler) Reconcile(ctx context.Context, req ctrl.Request)
100102

101103
log.Info("Reconciling Crossplane", "name", crossplane.Name, "namespace", crossplane.Namespace)
102104

105+
// Get ProviderConfig from Platform cluster
106+
pc := &v1alpha1.ProviderConfig{}
107+
if err := r.PlatformCluster.Client().Get(ctx, types.NamespacedName{Name: "default"}, pc); err != nil {
108+
return requeueEntry.Error(err)
109+
}
110+
103111
// Setup reconciliation context
104-
ctx, err := r.setupReconciliationContext(ctx, req)
112+
ctx, err := r.setupReconciliationContext(ctx, req, pc)
105113
if err != nil {
106114
return requeueEntry.Error(err)
107115
}
@@ -126,7 +134,7 @@ func (r *CrossplaneReconciler) Reconcile(ctx context.Context, req ctrl.Request)
126134
return requeueEntry.Error(err)
127135
}
128136

129-
return r.reconcileCrossplaneInstance(ctx, crossplane, mcpCluster.Client())
137+
return r.reconcileCrossplaneInstance(ctx, mcpCluster.Client(), crossplane, pc)
130138
}
131139

132140
func (r *CrossplaneReconciler) updateStatus(ctx context.Context, crossplane *v1alpha1.Crossplane, newConditions *[]metav1.Condition) {
@@ -140,13 +148,7 @@ func (r *CrossplaneReconciler) updateStatus(ctx context.Context, crossplane *v1a
140148
}
141149
}
142150

143-
func (r *CrossplaneReconciler) setupReconciliationContext(ctx context.Context, req ctrl.Request) (context.Context, error) {
144-
// Get ProviderConfig from Platform cluster
145-
providerConfig := &v1alpha1.ProviderConfig{}
146-
if err := r.PlatformCluster.Client().Get(ctx, types.NamespacedName{Name: "default"}, providerConfig); err != nil {
147-
return ctx, fmt.Errorf("unable to fetch ProviderConfig 'default': %w", err)
148-
}
149-
151+
func (r *CrossplaneReconciler) setupReconciliationContext(ctx context.Context, req ctrl.Request, providerConfig *v1alpha1.ProviderConfig) (context.Context, error) {
150152
// Handle ProviderConfig as ReleaseChannel
151153
resolverFn := r.GetResolverFunc(providerConfig)
152154
ctx = rcontext.WithVersionResolver(ctx, resolverFn)
@@ -210,16 +212,16 @@ func (r *CrossplaneReconciler) setupFluxKubeconfig(ctx context.Context, req ctrl
210212
return ctx, nil
211213
}
212214

213-
func (r *CrossplaneReconciler) reconcileCrossplaneInstance(ctx context.Context, crossplane *v1alpha1.Crossplane, mcpClient client.Client) (ctrl.Result, error) {
215+
func (r *CrossplaneReconciler) reconcileCrossplaneInstance(ctx context.Context, mcpClient client.Client, xp *v1alpha1.Crossplane, pc *v1alpha1.ProviderConfig) (ctrl.Result, error) {
214216
log := log.FromContext(ctx)
215217
requeueEntry := smartrequeue.FromContext(ctx)
216218

217219
// Handle deletion
218-
if !crossplane.DeletionTimestamp.IsZero() {
219-
return r.deleteCrossplaneInstance(ctx, crossplane, mcpClient)
220+
if !xp.DeletionTimestamp.IsZero() {
221+
return r.deleteCrossplaneInstance(ctx, mcpClient, xp, pc)
220222
}
221223

222-
conditions, err := r.createOrUpdateCrossplaneInstance(ctx, crossplane, mcpClient)
224+
conditions, err := r.createOrUpdateCrossplaneInstance(ctx, mcpClient, xp, pc)
223225
if err != nil {
224226
log.Error(err, "failed to create or update Crossplane instance")
225227
return requeueEntry.Error(err)
@@ -231,13 +233,13 @@ func (r *CrossplaneReconciler) reconcileCrossplaneInstance(ctx context.Context,
231233
condApi.SetStatusCondition(&newConditions, c)
232234
}
233235

234-
r.updateStatus(ctx, crossplane, &newConditions)
236+
r.updateStatus(ctx, xp, &newConditions)
235237

236238
// Successfully reconciled - reset the requeue backoff
237239
return requeueEntry.Reset()
238240
}
239241

240-
func (r *CrossplaneReconciler) deleteCrossplaneInstance(ctx context.Context, xp *v1alpha1.Crossplane, mcpClient client.Client) (ctrl.Result, error) {
242+
func (r *CrossplaneReconciler) deleteCrossplaneInstance(ctx context.Context, mcpClient client.Client, xp *v1alpha1.Crossplane, pc *v1alpha1.ProviderConfig) (ctrl.Result, error) {
241243
requeueEntry := smartrequeue.FromContext(ctx)
242244

243245
if !r.hasFinalizer(xp) {
@@ -247,7 +249,7 @@ func (r *CrossplaneReconciler) deleteCrossplaneInstance(ctx context.Context, xp
247249

248250
log := log.FromContext(ctx)
249251

250-
conditions, err := r.deleteControlPlaneComponents(ctx, xp, mcpClient)
252+
conditions, err := r.deleteControlPlaneComponents(ctx, mcpClient, xp, pc)
251253

252254
// Update status with current conditions
253255
newConditions := []metav1.Condition{}
@@ -272,12 +274,15 @@ func (r *CrossplaneReconciler) deleteCrossplaneInstance(ctx context.Context, xp
272274
return requeueEntry.Never()
273275
}
274276

275-
func (r *CrossplaneReconciler) deleteControlPlaneComponents(ctx context.Context, xp *v1alpha1.Crossplane, mcpClient client.Client) ([]metav1.Condition, error) {
277+
func (r *CrossplaneReconciler) deleteControlPlaneComponents(ctx context.Context, mcpClient client.Client, xp *v1alpha1.Crossplane, pc *v1alpha1.ProviderConfig) ([]metav1.Condition, error) {
276278
// disable all components
277279
xpCopy := xp.DeepCopy()
278280
xpCopy.Spec = v1alpha1.CrossplaneSpec{}
281+
// disable imagePullSecrets from ProviderConfig
282+
pcCopy := pc.DeepCopy()
283+
pcCopy.Spec = v1alpha1.ProviderConfigSpec{}
279284

280-
j, err := r.newJuggler(ctx, xpCopy, mcpClient)
285+
j, err := r.newJuggler(ctx, mcpClient, xpCopy, pcCopy)
281286
if err != nil {
282287
return nil, err
283288
}
@@ -307,8 +312,8 @@ func (r *CrossplaneReconciler) deleteControlPlaneComponents(ctx context.Context,
307312
return conditions, nil
308313
}
309314

310-
func (r *CrossplaneReconciler) createOrUpdateCrossplaneInstance(ctx context.Context, crossplane *v1alpha1.Crossplane, mcpClient client.Client) ([]metav1.Condition, error) {
311-
j, err := r.newJuggler(ctx, crossplane, mcpClient)
315+
func (r *CrossplaneReconciler) createOrUpdateCrossplaneInstance(ctx context.Context, mcpClient client.Client, xp *v1alpha1.Crossplane, pc *v1alpha1.ProviderConfig) ([]metav1.Condition, error) {
316+
j, err := r.newJuggler(ctx, mcpClient, xp, pc)
312317
if err != nil {
313318
return nil, err
314319
}
@@ -327,15 +332,45 @@ func (r *CrossplaneReconciler) createOrUpdateCrossplaneInstance(ctx context.Cont
327332
return conditions, nil
328333
}
329334

330-
func (r *CrossplaneReconciler) newJuggler(ctx context.Context, xp *v1alpha1.Crossplane, mcpClient client.Client) (*juggler.Juggler, error) {
335+
func (r *CrossplaneReconciler) newJuggler(ctx context.Context, mcpClient client.Client, xp *v1alpha1.Crossplane, pc *v1alpha1.ProviderConfig) (*juggler.Juggler, error) {
331336
logger := log.FromContext(ctx)
332-
comps := []juggler.Component{}
337+
var comps []juggler.Component
333338
jug := juggler.NewJuggler(logger, juggler.NewEventRecorder(r.Recorder, xp))
334339

335340
xpComp := &component.Crossplane{
336341
Config: &xp.Spec,
337342
}
338343
comps = append(comps, xpComp)
344+
345+
// Add image pull secrets as components to be managed by the juggler.
346+
// Target secret namespace is crossplane-system
347+
if pc.Spec.ImagePullSecrets != nil {
348+
podNs := os.Getenv(openmcpconsts.EnvVariablePodNamespace)
349+
if podNs == "" {
350+
return nil, errors.New("environment variable POD_NAMESPACE not set")
351+
}
352+
for _, ps := range pc.Spec.ImagePullSecrets {
353+
if ps.Name == "" {
354+
continue
355+
}
356+
sec := &component.Secret{
357+
SourceClient: r.PlatformCluster.Client(),
358+
Source: types.NamespacedName{
359+
Name: ps.Name,
360+
Namespace: podNs,
361+
},
362+
Target: types.NamespacedName{
363+
Name: ps.Name,
364+
Namespace: component.CrossplaneNamespace,
365+
},
366+
Enabled: xpComp.IsEnabled(),
367+
}
368+
comps = append(comps, sec)
369+
// Set image pull secret reference in Crossplane component so it can be added to the PodSpec
370+
xpComp.ImagePullSecretNames = append(xpComp.ImagePullSecretNames, sec.Target.Name)
371+
}
372+
}
373+
339374
if xp.Spec.Providers != nil {
340375
for _, provider := range xp.Spec.Providers {
341376
xpp := &component.CrossplaneProvider{
@@ -366,6 +401,7 @@ func (r *CrossplaneReconciler) registerReconcilers(juggler *juggler.Juggler, log
366401

367402
or := object.NewReconciler(logger, mcpClient, sputils.LabelComponentName)
368403
or.RegisterType(
404+
&component.Secret{},
369405
&component.CrossplaneProvider{},
370406
)
371407
juggler.RegisterReconciler(or)

pkg/component/crossplane_component.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ var _ components.TargetComponent = &Crossplane{}
3939

4040
// Crossplane represents the Crossplane component configuration.
4141
type Crossplane struct {
42-
Config *v1alpha1.CrossplaneSpec
43-
ChartSpec *v1beta1.ChartSpec
44-
Values *apiextensionsv1.JSON `json:"values,omitempty"`
42+
Config *v1alpha1.CrossplaneSpec
43+
ChartSpec *v1beta1.ChartSpec
44+
Values *apiextensionsv1.JSON `json:"values,omitempty"`
45+
ImagePullSecretNames []string
4546
}
4647

4748
// GetNamespace implements TargetComponent.
@@ -161,6 +162,9 @@ func (c *Crossplane) applyDefaultValues() error {
161162
}
162163
}
163164

165+
// Add imagePullSecrets if provided in ProviderConfig spec
166+
values["imagePullSecrets"] = c.ImagePullSecretNames
167+
164168
// Write updated values
165169
encoded, err := json.Marshal(values)
166170
c.Values = &apiextensionsv1.JSON{Raw: encoded}

pkg/component/crossplane_component_test.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ import (
1313

1414
func Test_Crossplane(t *testing.T) {
1515
testCases := []struct {
16-
desc string
17-
config *v1alpha1.CrossplaneSpec
18-
configValues *apiextensionsv1.JSON
19-
versionResolver v1beta1.VersionResolverFn
20-
validationFuncs []validationFunc
16+
desc string
17+
config *v1alpha1.CrossplaneSpec
18+
configValues *apiextensionsv1.JSON
19+
imagePullSecretNames []string
20+
versionResolver v1beta1.VersionResolverFn
21+
validationFuncs []validationFunc
2122
}{
2223
{
2324
desc: "should be disabled",
@@ -43,22 +44,25 @@ func Test_Crossplane(t *testing.T) {
4344
config: &v1alpha1.CrossplaneSpec{
4445
Version: "1.2.3",
4546
},
46-
configValues: &apiextensionsv1.JSON{Raw: []byte(`{"replicas":2}`)},
47-
versionResolver: fakeVersionResolver(false),
47+
configValues: &apiextensionsv1.JSON{Raw: []byte(`{"replicas":2}`)},
48+
imagePullSecretNames: []string{"secret-1", "secret-2"},
49+
versionResolver: fakeVersionResolver(false),
4850
validationFuncs: []validationFunc{
4951
hasName("Crossplane"),
5052
isEnabled(true),
5153
isAllowed(true),
5254
hasPreUninstallHook(),
5355
hasDependencies(0),
5456
isTargetComponent(
55-
hasNamespace("crossplane-system"),
57+
hasNamespace(CrossplaneNamespace),
5658
),
5759
isFluxComponent(
5860
returnsHelmRepo(),
5961
returnsHelmRelease(
6062
hasKubeconfigRef(),
6163
hasHelmValue(2, "replicas"), // custom value
64+
hasHelmValue("secret-1", "imagePullSecrets", "0"),
65+
hasHelmValue("secret-2", "imagePullSecrets", "1"),
6266
),
6367
),
6468
},
@@ -68,8 +72,9 @@ func Test_Crossplane(t *testing.T) {
6872
t.Run(tC.desc, func(t *testing.T) {
6973
ctx := newContext(tC.versionResolver)
7074
c := &Crossplane{
71-
Config: tC.config,
72-
Values: tC.configValues,
75+
Config: tC.config,
76+
Values: tC.configValues,
77+
ImagePullSecretNames: tC.imagePullSecretNames,
7378
}
7479
for _, vfn := range tC.validationFuncs {
7580
vfn(t, ctx, c)

0 commit comments

Comments
 (0)