Skip to content

Commit 9c523ad

Browse files
authored
feat: also copy image pull secrets referencedin PlatformService spec (#29)
1 parent 8cbbbca commit 9c523ad

File tree

15 files changed

+210
-36
lines changed

15 files changed

+210
-36
lines changed

api/install/install.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
fluxsourcev1 "github.com/fluxcd/source-controller/api/v1"
1111

1212
clustersv1alpha1 "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1"
13+
providerv1alpha1 "github.com/openmcp-project/openmcp-operator/api/provider/v1alpha1"
1314

1415
dnsv1alpha1 "github.com/openmcp-project/platform-service-dns/api/dns/v1alpha1"
1516
)
@@ -27,6 +28,7 @@ func InstallOperatorAPIsPlatform(scheme *runtime.Scheme) *runtime.Scheme {
2728
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
2829
utilruntime.Must(dnsv1alpha1.AddToScheme(scheme))
2930
utilruntime.Must(clustersv1alpha1.AddToScheme(scheme))
31+
utilruntime.Must(providerv1alpha1.AddToScheme(scheme))
3032
utilruntime.Must(fluxsourcev1.AddToScheme(scheme))
3133
utilruntime.Must(fluxhelmv2.AddToScheme(scheme))
3234

docs/config/dns-service-config.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ spec:
4141
4242
#### Secret Copying
4343
44-
The `secretsToCopy` field allows to specify secrets that should be copied. The source of the secrets is always the provider namespace on the platform cluster.
44+
The `secretsToCopy` field allows to specify secrets that should be copied (in addition to the image pull secrets from the `PlatformService` resource). The source of the secrets is always the provider namespace on the platform cluster.
4545

4646
Secrets referenced in `secretsToCopy.toPlatformCluster` will be copied into the reconciled `Cluster` resource's namespace on the platform cluster. This is the namespace that will host the Flux source resource and where pull secrets for the helm chart have to reside.
4747

@@ -51,6 +51,8 @@ In both cases, if the entry's `target` field is set, the secret will be renamed
5151

5252
If a secret that is to be created by the copy mechanism already exists, but is not managed by this controller (identified via labels), this will result in an error.
5353

54+
⚠️ Note that the secrets referenced in `spec.imagePullSecrets` of the `PlatformService` resource will always be copied to both, platform cluster and target cluster.
55+
5456
⚠️ **Warning: This mechanism can copy secrets to other namespaces and even other clusters, therefore potentially making them accessible to users which do not have permissions to access the source secret. Use with caution!**
5557

5658
#### Helm Chart Source

internal/controllers/cluster/controller.go

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
clusterconst "github.com/openmcp-project/openmcp-operator/api/clusters/v1alpha1/constants"
4343
commonapi "github.com/openmcp-project/openmcp-operator/api/common"
4444
openmcpconst "github.com/openmcp-project/openmcp-operator/api/constants"
45+
providerv1alpha1 "github.com/openmcp-project/openmcp-operator/api/provider/v1alpha1"
4546
accesslib "github.com/openmcp-project/openmcp-operator/lib/clusteraccess"
4647

4748
dnsv1alpha1 "github.com/openmcp-project/platform-service-dns/api/dns/v1alpha1"
@@ -99,6 +100,8 @@ type ReconcileResult struct {
99100
Message string
100101
// ProviderConfig is the complete provider configuration.
101102
ProviderConfig *dnsv1alpha1.DNSServiceConfig
103+
// PlatformService is the DNS PlatformService resource.
104+
PlatformService *providerv1alpha1.PlatformService
102105
}
103106

104107
func (r *ClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
@@ -173,6 +176,13 @@ func (r *ClusterReconciler) reconcile(ctx context.Context, c *clustersv1alpha1.C
173176
}
174177
return rr
175178
}
179+
// load PlatformService resource
180+
rr.PlatformService = &providerv1alpha1.PlatformService{}
181+
rr.PlatformService.Name = r.ProviderName
182+
if err := r.PlatformCluster.Client().Get(ctx, client.ObjectKeyFromObject(rr.PlatformService), rr.PlatformService); err != nil {
183+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error getting PlatformService '%s': %w", rr.PlatformService.Name, err), clusterconst.ReasonPlatformClusterInteractionProblem)
184+
return rr
185+
}
176186

177187
// iterate over configurations with purpose selectors and choose the first matching one
178188
for i, cfg := range rr.ProviderConfig.Spec.ExternalDNSForPurposes {
@@ -450,64 +460,74 @@ func (r *ClusterReconciler) copySecrets(ctx context.Context, namespace string, e
450460

451461
// copy secrets if configured
452462
if rr.ProviderConfig.Spec.SecretsToCopy != nil {
453-
var secretsToCopy []dnsv1alpha1.SecretCopy
463+
var configuredSecretsToCopy []dnsv1alpha1.SecretCopy
454464
var targetAccess client.Client
455465
var interactionProblemReason string
456466
switch copyTarget {
457467
case platformClusterCopy:
458-
secretsToCopy = rr.ProviderConfig.Spec.SecretsToCopy.ToPlatformCluster
468+
configuredSecretsToCopy = rr.ProviderConfig.Spec.SecretsToCopy.ToPlatformCluster
459469
targetAccess = r.PlatformCluster.Client()
460470
interactionProblemReason = clusterconst.ReasonPlatformClusterInteractionProblem
461471
case targetClusterCopy:
462-
secretsToCopy = rr.ProviderConfig.Spec.SecretsToCopy.ToTargetCluster
472+
configuredSecretsToCopy = rr.ProviderConfig.Spec.SecretsToCopy.ToTargetCluster
463473
targetAccess = rr.Access.Client()
464474
interactionProblemReason = dnsv1alpha1.ReasonTargetClusterInteractionProblem
465475
}
476+
secretsToCopy := map[string]string{}
477+
for _, ips := range rr.PlatformService.Spec.ImagePullSecrets {
478+
secretsToCopy[ips.Name] = ips.Name
479+
}
480+
for _, sc := range configuredSecretsToCopy {
481+
tName := ""
482+
if sc.Target != nil && sc.Target.Name != "" {
483+
tName = sc.Target.Name
484+
} else {
485+
tName = sc.Source.Name
486+
}
487+
secretsToCopy[sc.Source.Name] = tName
488+
}
466489
if targetAccess == nil {
467490
rr.ReconcileError = errutils.WithReason(fmt.Errorf("no access to cluster '%s' for copying secrets", copyTarget), clusterconst.ReasonInternalError)
468491
return rr, copied
469492
}
470-
for i, stc := range secretsToCopy {
493+
for sourceName, targetName := range secretsToCopy {
471494
source := &corev1.Secret{}
472-
source.Name = stc.Source.Name
495+
source.Name = sourceName
473496
source.Namespace = r.ProviderNamespace
474-
log.Debug("Secret copying configured, getting source secret", "sourceNamespace", source.Namespace, "sourceName", source.Name, "index", i)
497+
log.Debug("Secret copying configured, getting source secret", "sourceNamespace", source.Namespace, "sourceName", source.Name)
475498
if err := r.PlatformCluster.Client().Get(ctx, client.ObjectKeyFromObject(source), source); err != nil {
476-
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error getting source secret '%s/%s' (index: %d): %w", source.Namespace, source.Name, i, err), clusterconst.ReasonPlatformClusterInteractionProblem)
499+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error getting source secret '%s/%s': %w", source.Namespace, source.Name, err), clusterconst.ReasonPlatformClusterInteractionProblem)
477500
return rr, copied
478501
}
479502

480503
// check if target secret already exists
481504
target := &corev1.Secret{}
482-
target.Name = source.Name
483-
if stc.Target != nil && stc.Target.Name != "" {
484-
target.Name = stc.Target.Name
485-
}
505+
target.Name = targetName
486506
target.Namespace = namespace
487507
if copyTarget == platformClusterCopy && target.Namespace == source.Namespace && target.Name == source.Name {
488-
log.Debug("Skipping copying of secret because source and target are identical", "secretNamespace", target.Namespace, "secretName", target.Name, "index", i)
508+
log.Debug("Skipping copying of secret because source and target are identical", "secretNamespace", target.Namespace, "secretName", target.Name)
489509
copied.Insert(target.Name)
490510
continue
491511
}
492512
targetExists := true
493513
if err := targetAccess.Get(ctx, client.ObjectKeyFromObject(target), target); err != nil {
494514
if !apierrors.IsNotFound(err) {
495-
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error getting target secret '%s/%s' (index: %d): %w", target.Namespace, target.Name, i, err), interactionProblemReason)
515+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error getting target secret '%s/%s': %w", target.Namespace, target.Name, err), interactionProblemReason)
496516
return rr, copied
497517
}
498518
targetExists = false
499519
}
500520
if targetExists {
501521
// if target secret exists, verify that it is managed by us
502-
log.Debug("Target secret already exists", "targetNamespace", target.Namespace, "targetName", target.Name, "index", i)
522+
log.Debug("Target secret already exists", "targetNamespace", target.Namespace, "targetName", target.Name)
503523
for k, v := range expectedLabels {
504524
if v2, ok := target.Labels[k]; !ok || v2 != v {
505-
rr.ReconcileError = errutils.WithReason(fmt.Errorf("target secret '%s/%s' (index: %d) already exists and is not managed by %s controller", target.Namespace, target.Name, i, ControllerName), clusterconst.ReasonConfigurationProblem)
525+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("target secret '%s/%s' already exists and is not managed by %s controller", target.Namespace, target.Name, ControllerName), clusterconst.ReasonConfigurationProblem)
506526
return rr, copied
507527
}
508528
}
509529
}
510-
log.Debug("Creating or updating target secret", "targetNamespace", target.Namespace, "targetName", target.Name, "index", i)
530+
log.Debug("Creating or updating target secret", "targetNamespace", target.Namespace, "targetName", target.Name)
511531
if _, err := controllerutil.CreateOrUpdate(ctx, targetAccess, target, func() error {
512532
target.Labels = maputils.Merge(target.Labels, source.Labels, expectedLabels)
513533
target.Annotations = maputils.Merge(target.Annotations, source.Annotations)
@@ -516,7 +536,7 @@ func (r *ClusterReconciler) copySecrets(ctx context.Context, namespace string, e
516536
target.Type = source.Type
517537
return nil
518538
}); err != nil {
519-
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error creating or updating target secret '%s/%s' (index: %d): %w", target.Namespace, target.Name, i, err), interactionProblemReason)
539+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error creating or updating target secret '%s/%s': %w", target.Namespace, target.Name, err), interactionProblemReason)
520540
return rr, copied
521541
}
522542
copied.Insert(target.Name)

internal/controllers/cluster/controller_test.go

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -324,9 +324,6 @@ var _ = Describe("ClusterReconciler", func() {
324324
It("should use finalizers and remove resources when the Cluster is being deleted", func() {
325325
env, _ := defaultTestSetup("testdata", "test-01")
326326

327-
cfg := &dnsv1alpha1.DNSServiceConfig{}
328-
Expect(env.Client().Get(env.Ctx, client.ObjectKey{Name: providerName}, cfg)).To(Succeed())
329-
330327
c1 := &clustersv1alpha1.Cluster{}
331328
Expect(env.Client().Get(env.Ctx, client.ObjectKey{Name: "cluster-01", Namespace: "foo"}, c1)).To(Succeed())
332329
rr := env.ShouldReconcile(testutils.RequestFromObject(c1))
@@ -394,9 +391,6 @@ var _ = Describe("ClusterReconciler", func() {
394391
It("should delete obsolete flux sources", func() {
395392
env, _ := defaultTestSetup("testdata", "test-01")
396393

397-
cfg := &dnsv1alpha1.DNSServiceConfig{}
398-
Expect(env.Client().Get(env.Ctx, client.ObjectKey{Name: providerName}, cfg)).To(Succeed())
399-
400394
// create dummy flux sources
401395
expectedLabels := map[string]string{
402396
openmcpconst.ManagedByLabel: managedByValue,
@@ -438,9 +432,6 @@ var _ = Describe("ClusterReconciler", func() {
438432
It("should create a GitRepository if configured", func() {
439433
env, _ := defaultTestSetup("testdata", "test-03")
440434

441-
cfg := &dnsv1alpha1.DNSServiceConfig{}
442-
Expect(env.Client().Get(env.Ctx, client.ObjectKey{Name: providerName}, cfg)).To(Succeed())
443-
444435
c1 := &clustersv1alpha1.Cluster{}
445436
Expect(env.Client().Get(env.Ctx, client.ObjectKey{Name: "cluster-01", Namespace: "foo"}, c1)).To(Succeed())
446437
rr := env.ShouldReconcile(testutils.RequestFromObject(c1))
@@ -471,9 +462,6 @@ var _ = Describe("ClusterReconciler", func() {
471462
It("should create a HelmRepository if configured", func() {
472463
env, _ := defaultTestSetup("testdata", "test-04")
473464

474-
cfg := &dnsv1alpha1.DNSServiceConfig{}
475-
Expect(env.Client().Get(env.Ctx, client.ObjectKey{Name: providerName}, cfg)).To(Succeed())
476-
477465
c1 := &clustersv1alpha1.Cluster{}
478466
Expect(env.Client().Get(env.Ctx, client.ObjectKey{Name: "cluster-01", Namespace: "foo"}, c1)).To(Succeed())
479467
rr := env.ShouldReconcile(testutils.RequestFromObject(c1))
@@ -504,9 +492,6 @@ var _ = Describe("ClusterReconciler", func() {
504492
It("should replace the special keywords in the values correctly", func() {
505493
env, _ := defaultTestSetup("testdata", "test-03")
506494

507-
cfg := &dnsv1alpha1.DNSServiceConfig{}
508-
Expect(env.Client().Get(env.Ctx, client.ObjectKey{Name: providerName}, cfg)).To(Succeed())
509-
510495
c1 := &clustersv1alpha1.Cluster{}
511496
Expect(env.Client().Get(env.Ctx, client.ObjectKey{Name: "cluster-01", Namespace: "foo"}, c1)).To(Succeed())
512497
rr := env.ShouldReconcile(testutils.RequestFromObject(c1))
@@ -535,9 +520,6 @@ var _ = Describe("ClusterReconciler", func() {
535520
It("should not copy secrets if source and destination are identical", func() {
536521
env, _ := defaultTestSetup("testdata", "test-05")
537522

538-
cfg := &dnsv1alpha1.DNSServiceConfig{}
539-
Expect(env.Client().Get(env.Ctx, client.ObjectKey{Name: providerName}, cfg)).To(Succeed())
540-
541523
c1 := &clustersv1alpha1.Cluster{}
542524
Expect(env.Client().Get(env.Ctx, client.ObjectKey{Name: "cluster-01", Namespace: "test"}, c1)).To(Succeed())
543525
s1 := &corev1.Secret{}
@@ -554,6 +536,64 @@ var _ = Describe("ClusterReconciler", func() {
554536
Expect(s1.Labels).To(BeEmpty())
555537
})
556538

539+
It("should copy image pull secrets from the PlatformService resource", func() {
540+
env, rec := defaultTestSetup("testdata", "test-06")
541+
542+
c1 := &clustersv1alpha1.Cluster{}
543+
Expect(env.Client().Get(env.Ctx, client.ObjectKey{Name: "cluster-01", Namespace: "foo"}, c1)).To(Succeed())
544+
545+
rr := env.ShouldReconcile(testutils.RequestFromObject(c1))
546+
Expect(rr.RequeueAfter).To(BeNumerically(">", 0))
547+
fakeAccessRequestReadiness(env, c1)
548+
rr = env.ShouldReconcile(testutils.RequestFromObject(c1))
549+
Expect(rr.RequeueAfter).To(BeZero())
550+
551+
expectedLabels := map[string]string{
552+
openmcpconst.ManagedByLabel: managedByValue,
553+
openmcpconst.ManagedPurposeLabel: "cluster-01",
554+
}
555+
ss := &corev1.SecretList{}
556+
// copied secrets on platform cluster
557+
Expect(env.Client().List(env.Ctx, ss, client.InNamespace(c1.Namespace), client.MatchingLabels(expectedLabels))).To(Succeed())
558+
Expect(ss.Items).To(ConsistOf(
559+
MatchFields(IgnoreExtras, Fields{
560+
"ObjectMeta": MatchFields(IgnoreExtras, Fields{
561+
"Name": Equal("my-auth-copy"),
562+
}),
563+
"Data": Equal(map[string][]byte{"key": []byte("value")}),
564+
}),
565+
MatchFields(IgnoreExtras, Fields{
566+
"ObjectMeta": MatchFields(IgnoreExtras, Fields{
567+
"Name": Equal("another-auth"),
568+
}),
569+
"Data": Equal(map[string][]byte{"auth": []byte("asdf")}),
570+
}),
571+
MatchFields(IgnoreExtras, Fields{
572+
"ObjectMeta": MatchFields(IgnoreExtras, Fields{
573+
"Name": Equal("my-other-secret"),
574+
}),
575+
"Data": Equal(map[string][]byte{"foo": []byte("bar")}),
576+
}),
577+
))
578+
// copied secrets on target cluster
579+
Expect(rec.FakeClientMappings["foo/cluster-01"].List(env.Ctx, ss, client.InNamespace(cluster.TargetClusterNamespace), client.MatchingLabels(expectedLabels))).To(Succeed())
580+
Expect(ss.Items).To(ConsistOf(
581+
MatchFields(IgnoreExtras, Fields{
582+
"ObjectMeta": MatchFields(IgnoreExtras, Fields{
583+
"Name": Equal("my-auth"),
584+
}),
585+
"Data": Equal(map[string][]byte{"key": []byte("value")}),
586+
}),
587+
MatchFields(IgnoreExtras, Fields{
588+
"ObjectMeta": MatchFields(IgnoreExtras, Fields{
589+
"Name": Equal("another-auth"),
590+
}),
591+
"Data": Equal(map[string][]byte{"auth": []byte("asdf")}),
592+
}),
593+
))
594+
595+
})
596+
557597
})
558598

559599
func fakeAccessRequestReadiness(env *testutils.Environment, c *clustersv1alpha1.Cluster) {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: openmcp.cloud/v1alpha1
2+
kind: PlatformService
3+
metadata:
4+
name: dns-service
5+
spec:
6+
image: example.com/platform-service-dns:latest
7+
verbosity: INFO
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: openmcp.cloud/v1alpha1
2+
kind: PlatformService
3+
metadata:
4+
name: dns-service
5+
spec:
6+
image: example.com/platform-service-dns:latest
7+
verbosity: INFO
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: openmcp.cloud/v1alpha1
2+
kind: PlatformService
3+
metadata:
4+
name: dns-service
5+
spec:
6+
image: example.com/platform-service-dns:latest
7+
verbosity: INFO
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: openmcp.cloud/v1alpha1
2+
kind: PlatformService
3+
metadata:
4+
name: dns-service
5+
spec:
6+
image: example.com/platform-service-dns:latest
7+
verbosity: INFO
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: openmcp.cloud/v1alpha1
2+
kind: PlatformService
3+
metadata:
4+
name: dns-service
5+
spec:
6+
image: example.com/platform-service-dns:latest
7+
verbosity: INFO
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: clusters.openmcp.cloud/v1alpha1
2+
kind: Cluster
3+
metadata:
4+
name: cluster-01
5+
namespace: foo
6+
spec:
7+
kubernetes: {}
8+
profile: my-profile
9+
purposes:
10+
- foo
11+
- bar
12+
tenancy: Shared

0 commit comments

Comments
 (0)