Skip to content

Commit fbbd727

Browse files
committed
enable copying any amount of secrets
1 parent 5ee1320 commit fbbd727

File tree

8 files changed

+206
-120
lines changed

8 files changed

+206
-120
lines changed

api/crds/manifests/dns.openmcp.cloud_dnsserviceconfigs.yaml

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,14 @@ spec:
5959
If not set, the global HelmReleaseReconciliationInterval is used.
6060
type: string
6161
helmValues:
62-
description: HelmValues are the helm values to deploy external-dns
63-
with, if the purpose selector matches.
62+
description: |-
63+
HelmValues are the helm values to deploy external-dns with, if the purpose selector matches.
64+
There are a few special strings which will be replaced before creating the HelmRelease:
65+
- <provider.name> will be replaced with the provider name resource.
66+
- <provider.namespace> will be replaced with the namespace that hosts the platform service.
67+
- <environment> will be replaced with the environment name of the operator.
68+
- <cluster.name> will be replaced with the name of the reconciled Cluster.
69+
- <cluster.namespace> will be replaced with the namespace of the reconciled Cluster.
6470
type: string
6571
name:
6672
description: |-
@@ -100,45 +106,6 @@ spec:
100106
When using a source that needs a version (helm or oci), append the version to the chart name using '@', e.g. '[email protected]' or omit for latest version.
101107
minLength: 1
102108
type: string
103-
copyAuthSecret:
104-
description: |-
105-
SecretCopy defines the name of the secret to copy and the name of the copied secret.
106-
If target is nil or target.name is empty, the secret will be copied with the same name as the source secret.
107-
properties:
108-
source:
109-
description: LocalObjectReference is a reference to an object
110-
in the same namespace as the resource referencing it.
111-
properties:
112-
name:
113-
default: ""
114-
description: |-
115-
Name of the referent.
116-
This field is effectively required, but due to backwards compatibility is
117-
allowed to be empty. Instances of this type with an empty value here are
118-
almost certainly wrong.
119-
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
120-
type: string
121-
type: object
122-
x-kubernetes-map-type: atomic
123-
target:
124-
description: LocalObjectReference is a reference to an object
125-
in the same namespace as the resource referencing it.
126-
properties:
127-
name:
128-
default: ""
129-
description: |-
130-
Name of the referent.
131-
This field is effectively required, but due to backwards compatibility is
132-
allowed to be empty. Instances of this type with an empty value here are
133-
almost certainly wrong.
134-
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
135-
type: string
136-
type: object
137-
x-kubernetes-map-type: atomic
138-
required:
139-
- source
140-
- target
141-
type: object
142109
git:
143110
description: |-
144111
GitRepositorySpec specifies the required configuration to produce an
@@ -670,6 +637,50 @@ spec:
670637
The value can be overwritten for specific purposes using ExternalDNSForPurposes.
671638
If not set, a default of 1h is used.
672639
type: string
640+
secretsToCopy:
641+
description: |-
642+
SecretsToCopy specifies an optional list of secrets which will be copied from the provider namespace into the namespaces of the reconciled Clusters.
643+
This can, for example, be used to distribute credentials for the registry holding the external-dns helm chart.
644+
items:
645+
description: |-
646+
SecretCopy defines the name of the secret to copy and the name of the copied secret.
647+
If target is nil or target.name is empty, the secret will be copied with the same name as the source secret.
648+
properties:
649+
source:
650+
description: LocalObjectReference is a reference to an object
651+
in the same namespace as the resource referencing it.
652+
properties:
653+
name:
654+
default: ""
655+
description: |-
656+
Name of the referent.
657+
This field is effectively required, but due to backwards compatibility is
658+
allowed to be empty. Instances of this type with an empty value here are
659+
almost certainly wrong.
660+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
661+
type: string
662+
type: object
663+
x-kubernetes-map-type: atomic
664+
target:
665+
description: LocalObjectReference is a reference to an object
666+
in the same namespace as the resource referencing it.
667+
properties:
668+
name:
669+
default: ""
670+
description: |-
671+
Name of the referent.
672+
This field is effectively required, but due to backwards compatibility is
673+
allowed to be empty. Instances of this type with an empty value here are
674+
almost certainly wrong.
675+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
676+
type: string
677+
type: object
678+
x-kubernetes-map-type: atomic
679+
required:
680+
- source
681+
- target
682+
type: object
683+
type: array
673684
required:
674685
- externalDNSSource
675686
type: object

api/dns/v1alpha1/config_types.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ type DNSServiceConfigSpec struct {
1616
// ExternalDNSSource is the source of the external-dns helm chart.
1717
ExternalDNSSource ExternalDNSSource `json:"externalDNSSource"`
1818

19+
// SecretsToCopy specifies an optional list of secrets which will be copied from the provider namespace into the namespaces of the reconciled Clusters.
20+
// This can, for example, be used to distribute credentials for the registry holding the external-dns helm chart.
21+
// +optional
22+
SecretsToCopy []SecretCopy `json:"secretsToCopy,omitempty"`
23+
1924
// HelmReleaseReconciliationInterval is the interval at which the HelmRelease for external-dns is reconciled.
2025
// The value can be overwritten for specific purposes using ExternalDNSForPurposes.
2126
// If not set, a default of 1h is used.
@@ -38,11 +43,10 @@ type ExternalDNSSource struct {
3843
// Depending on the source, this can also be a relative path within the repository.
3944
// When using a source that needs a version (helm or oci), append the version to the chart name using '@', e.g. '[email protected]' or omit for latest version.
4045
// +kubebuilder:validation:MinLength=1
41-
ChartName string `json:"chartName"`
42-
Helm *fluxv1.HelmRepositorySpec `json:"helm,omitempty"`
43-
Git *fluxv1.GitRepositorySpec `json:"git,omitempty"`
44-
OCI *fluxv1.OCIRepositorySpec `json:"oci,omitempty"`
45-
CopyAuthSecret *SecretCopy `json:"copyAuthSecret,omitempty"`
46+
ChartName string `json:"chartName"`
47+
Helm *fluxv1.HelmRepositorySpec `json:"helm,omitempty"`
48+
Git *fluxv1.GitRepositorySpec `json:"git,omitempty"`
49+
OCI *fluxv1.OCIRepositorySpec `json:"oci,omitempty"`
4650
}
4751

4852
// SecretCopy defines the name of the secret to copy and the name of the copied secret.
@@ -70,6 +74,12 @@ type ExternalDNSPurposeConfig struct {
7074
HelmReleaseReconciliationInterval *metav1.Duration `json:"helmReleaseReconciliationInterval,omitempty"`
7175

7276
// HelmValues are the helm values to deploy external-dns with, if the purpose selector matches.
77+
// There are a few special strings which will be replaced before creating the HelmRelease:
78+
// - <provider.name> will be replaced with the provider name resource.
79+
// - <provider.namespace> will be replaced with the namespace that hosts the platform service.
80+
// - <environment> will be replaced with the environment name of the operator.
81+
// - <cluster.name> will be replaced with the name of the reconciled Cluster.
82+
// - <cluster.namespace> will be replaced with the namespace of the reconciled Cluster.
7383
// +kubebuilder:validation:Type=string
7484
// +kubebuilder:validation:Schemaless
7585
HelmValues *apiextensionsv1.JSON `json:"helmValues"`

api/dns/v1alpha1/zz_generated.deepcopy.go

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

internal/controllers/cluster/controller.go

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1616
"k8s.io/apimachinery/pkg/runtime/schema"
1717
"k8s.io/apimachinery/pkg/types"
18+
"k8s.io/apimachinery/pkg/util/sets"
1819
"k8s.io/client-go/tools/record"
1920
ctrl "sigs.k8s.io/controller-runtime"
2021
"sigs.k8s.io/controller-runtime/pkg/builder"
@@ -248,7 +249,12 @@ func (r *ClusterReconciler) handleCreateOrUpdate(ctx context.Context, c *cluster
248249
}
249250
rr.AccessRequest = ar
250251

251-
rr = r.deployAuthSecret(ctx, c, expectedLabels, rr)
252+
rr, copied := r.copySecrets(ctx, c, expectedLabels, rr)
253+
if rr.ReconcileError != nil || rr.Result.RequeueAfter > 0 {
254+
return rr
255+
}
256+
// remove any secrets that were copied in a previous run but are no longer configured to be copied
257+
rr = r.uncopySecrets(ctx, c, expectedLabels, rr, copied)
252258
if rr.ReconcileError != nil || rr.Result.RequeueAfter > 0 {
253259
return rr
254260
}
@@ -288,7 +294,7 @@ func (r *ClusterReconciler) handleDelete(ctx context.Context, c *clustersv1alpha
288294
return rr
289295
}
290296

291-
rr = r.undeployAuthSecret(ctx, c, expectedLabels, rr)
297+
rr = r.uncopySecrets(ctx, c, expectedLabels, rr, nil)
292298
if rr.ReconcileError != nil || rr.Result.RequeueAfter > 0 {
293299
return rr
294300
}
@@ -319,50 +325,52 @@ func (r *ClusterReconciler) handleDelete(ctx context.Context, c *clustersv1alpha
319325
return rr
320326
}
321327

322-
// deployAuthSecret copies the auth secret (for access to the helm chart source) into the Cluster namespace if configured.
323-
func (r *ClusterReconciler) deployAuthSecret(ctx context.Context, c *clustersv1alpha1.Cluster, expectedLabels map[string]string, rr ReconcileResult) ReconcileResult {
328+
// copySecrets copies the configured secrets into the Cluster namespace.
329+
// Returns a list of the names of the copied secrets.
330+
func (r *ClusterReconciler) copySecrets(ctx context.Context, c *clustersv1alpha1.Cluster, expectedLabels map[string]string, rr ReconcileResult) (ReconcileResult, sets.Set[string]) {
324331
log := logging.FromContextOrPanic(ctx)
332+
copied := sets.New[string]()
325333

326-
// copy secret if configured
327-
if rr.ProviderConfig.Spec.ExternalDNSSource.CopyAuthSecret != nil {
334+
// copy secrets if configured
335+
for i, stc := range rr.ProviderConfig.Spec.SecretsToCopy {
328336
source := &corev1.Secret{}
329-
source.Name = rr.ProviderConfig.Spec.ExternalDNSSource.CopyAuthSecret.Source.Name
337+
source.Name = stc.Source.Name
330338
source.Namespace = r.ProviderNamespace
331-
log.Debug("Auth secret copying configured, getting source secret", "sourceNamespace", source.Namespace, "sourceName", source.Name)
339+
log.Debug("Secret copying configured, getting source secret", "sourceNamespace", source.Namespace, "sourceName", source.Name, "index", i)
332340
if err := r.PlatformCluster.Client().Get(ctx, client.ObjectKeyFromObject(source), source); err != nil {
333-
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error getting source secret '%s/%s': %w", source.Namespace, source.Name, err), clusterconst.ReasonPlatformClusterInteractionProblem)
334-
return rr
341+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error getting source secret '%s/%s' (index: %d): %w", source.Namespace, source.Name, i, err), clusterconst.ReasonPlatformClusterInteractionProblem)
342+
return rr, copied
335343
}
336344

337345
// check if target secret already exists
338346
target := &corev1.Secret{}
339347
target.Name = source.Name
340-
if rr.ProviderConfig.Spec.ExternalDNSSource.CopyAuthSecret.Target != nil && rr.ProviderConfig.Spec.ExternalDNSSource.CopyAuthSecret.Target.Name != "" {
341-
target.Name = rr.ProviderConfig.Spec.ExternalDNSSource.CopyAuthSecret.Target.Name
348+
if stc.Target != nil && stc.Target.Name != "" {
349+
target.Name = stc.Target.Name
342350
}
343351
target.Namespace = c.Namespace
344352
targetExists := true
345353
if err := r.PlatformCluster.Client().Get(ctx, client.ObjectKeyFromObject(target), target); err != nil {
346354
if !apierrors.IsNotFound(err) {
347-
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error getting target secret '%s/%s': %w", target.Namespace, target.Name, err), clusterconst.ReasonPlatformClusterInteractionProblem)
348-
return rr
355+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error getting target secret '%s/%s' (index: %d): %w", target.Namespace, target.Name, i, err), clusterconst.ReasonPlatformClusterInteractionProblem)
356+
return rr, copied
349357
}
350358
targetExists = false
351359
}
352360
if targetExists {
353361
// if target secret exists, verify that it is managed by us
354-
log.Debug("Target secret already exists", "targetNamespace", target.Namespace, "targetName", target.Name)
362+
log.Debug("Target secret already exists", "targetNamespace", target.Namespace, "targetName", target.Name, "index", i)
355363
for k, v := range expectedLabels {
356364
if v2, ok := target.Labels[k]; !ok || v2 != v {
357-
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)
358-
return rr
365+
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)
366+
return rr, copied
359367
}
360368
}
361369
}
362-
log.Debug("Creating or updating target secret", "targetNamespace", target.Namespace, "targetName", target.Name)
370+
log.Debug("Creating or updating target secret", "targetNamespace", target.Namespace, "targetName", target.Name, "index", i)
363371
if _, err := controllerutil.CreateOrUpdate(ctx, r.PlatformCluster.Client(), target, func() error {
364372
if err := controllerutil.SetOwnerReference(c, target, r.PlatformCluster.Scheme()); err != nil {
365-
return fmt.Errorf("error setting owner reference on target secret '%s/%s': %w", target.Namespace, target.Name, err)
373+
return fmt.Errorf("error setting owner reference on target secret '%s/%s' (index: %d): %w", target.Namespace, target.Name, i, err)
366374
}
367375
target.Labels = maputils.Merge(target.Labels, source.Labels, expectedLabels)
368376
target.Annotations = maputils.Merge(target.Annotations, source.Annotations)
@@ -371,14 +379,14 @@ func (r *ClusterReconciler) deployAuthSecret(ctx context.Context, c *clustersv1a
371379
target.Type = source.Type
372380
return nil
373381
}); err != nil {
374-
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error creating or updating target secret '%s/%s': %w", target.Namespace, target.Name, err), clusterconst.ReasonPlatformClusterInteractionProblem)
375-
return rr
382+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error creating or updating target secret '%s/%s' (index: %d): %w", target.Namespace, target.Name, i, err), clusterconst.ReasonPlatformClusterInteractionProblem)
383+
return rr, copied
376384
}
377-
378-
rr.Message = "Successfully copied auth secret into Cluster namespace."
385+
copied.Insert(target.Name)
379386
}
387+
rr.Message = fmt.Sprintf("Successfully copied %d secrets into Cluster namespace", len(rr.ProviderConfig.Spec.SecretsToCopy))
380388

381-
return rr
389+
return rr, copied
382390
}
383391

384392
// deployHelmChartSource deploys the configured Flux source (HelmRepository, GitRepository, OCIRepository) into the Cluster namespace.
@@ -681,9 +689,10 @@ func (r *ClusterReconciler) undeployHelmChartSource(ctx context.Context, c *clus
681689
return rr
682690
}
683691

684-
// undeployAuthSecret removes all secrets from the Cluster namespace where the labels indicate they were created by this controller for the given Cluster.
692+
// uncopySecrets removes all secrets from the Cluster namespace where the labels indicate they were created by this controller for the given Cluster.
693+
// Secrets listed in 'keep' are not deleted.
685694
// It does not wait for their deletion.
686-
func (r *ClusterReconciler) undeployAuthSecret(ctx context.Context, c *clustersv1alpha1.Cluster, expectedLabels map[string]string, rr ReconcileResult) ReconcileResult {
695+
func (r *ClusterReconciler) uncopySecrets(ctx context.Context, c *clustersv1alpha1.Cluster, expectedLabels map[string]string, rr ReconcileResult, keep sets.Set[string]) ReconcileResult {
687696
log := logging.FromContextOrPanic(ctx)
688697

689698
// list existing secrets to detect obsolete ones
@@ -693,18 +702,26 @@ func (r *ClusterReconciler) undeployAuthSecret(ctx context.Context, c *clustersv
693702
return rr
694703
}
695704

705+
deleted := 0
706+
kept := 0
696707
for i := range existingSecrets.Items {
697708
obj := &existingSecrets.Items[i]
698-
log.Info("Deleting auth secret", "resourceName", obj.GetName(), "resourceNamespace", obj.GetNamespace())
709+
if keep.Has(obj.Name) {
710+
log.Debug("Keeping copied secret", "resourceName", obj.GetName(), "resourceNamespace", obj.GetNamespace())
711+
kept++
712+
continue
713+
}
714+
log.Info("Deleting copied secret", "resourceName", obj.GetName(), "resourceNamespace", obj.GetNamespace())
699715
if err := r.PlatformCluster.Client().Delete(ctx, obj); err != nil {
700716
if !apierrors.IsNotFound(err) {
701717
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error deleting Secret '%s/%s': %w", obj.GetNamespace(), obj.GetName(), err), clusterconst.ReasonPlatformClusterInteractionProblem)
702718
return rr
703719
}
704720
}
721+
deleted++
705722
}
706723

707-
rr.Message = "Deleted all auth secrets for Cluster."
724+
rr.Message = fmt.Sprintf("Deleted %d copied secrets from Cluster namespace, kept %d.", deleted, kept)
708725
return rr
709726
}
710727

0 commit comments

Comments
 (0)