1
14
15 package externalsecret
16
17 import (
18 "context"
19 "encoding/json"
20 "fmt"
21 "strings"
22 "time"
23
24 "github.com/go-logr/logr"
25 "github.com/prometheus/client_golang/prometheus"
26 v1 "k8s.io/api/core/v1"
27 "k8s.io/apimachinery/pkg/api/equality"
28 apierrors "k8s.io/apimachinery/pkg/api/errors"
29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30 "k8s.io/apimachinery/pkg/runtime"
31 "k8s.io/apimachinery/pkg/types"
32 "k8s.io/client-go/rest"
33 "k8s.io/client-go/tools/record"
34 ctrl "sigs.k8s.io/controller-runtime"
35 "sigs.k8s.io/controller-runtime/pkg/builder"
36 "sigs.k8s.io/controller-runtime/pkg/client"
37 "sigs.k8s.io/controller-runtime/pkg/controller"
38 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
39
40 esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
41
42 "github.com/external-secrets/external-secrets/pkg/controllers/externalsecret/esmetrics"
43 ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
44
45 _ "github.com/external-secrets/external-secrets/pkg/generator/register"
46
47 _ "github.com/external-secrets/external-secrets/pkg/provider/register"
48 "github.com/external-secrets/external-secrets/pkg/utils"
49 )
50
51 const (
52 fieldOwnerTemplate = "externalsecrets.external-secrets.io/%v"
53 errGetES = "could not get ExternalSecret"
54 errConvert = "could not apply conversion strategy to keys: %v"
55 errDecode = "could not apply decoding strategy to %v[%d]: %v"
56 errGenerate = "could not generate [%d]: %w"
57 errRewrite = "could not rewrite spec.dataFrom[%d]: %v"
58 errInvalidKeys = "secret keys from spec.dataFrom.%v[%d] can only have alphanumeric,'-', '_' or '.' characters. Convert them using rewrite (https://external-secrets.io/latest/guides-datafrom-rewrite)"
59 errUpdateSecret = "could not update Secret"
60 errPatchStatus = "unable to patch status"
61 errGetExistingSecret = "could not get existing secret: %w"
62 errSetCtrlReference = "could not set ExternalSecret controller reference: %w"
63 errFetchTplFrom = "error fetching templateFrom data: %w"
64 errGetSecretData = "could not get secret data from provider"
65 errDeleteSecret = "could not delete secret"
66 errApplyTemplate = "could not apply template: %w"
67 errExecTpl = "could not execute template: %w"
68 errInvalidCreatePolicy = "invalid creationPolicy=%s. Can not delete secret i do not own"
69 errPolicyMergeNotFound = "the desired secret %s was not found. With creationPolicy=Merge the secret won't be created"
70 errPolicyMergeGetSecret = "unable to get secret %s: %w"
71 errPolicyMergeMutate = "unable to mutate secret %s: %w"
72 errPolicyMergePatch = "unable to patch secret %s: %w"
73 )
74
75
76 type Reconciler struct {
77 client.Client
78 Log logr.Logger
79 Scheme *runtime.Scheme
80 RestConfig *rest.Config
81 ControllerClass string
82 RequeueInterval time.Duration
83 ClusterSecretStoreEnabled bool
84 EnableFloodGate bool
85 recorder record.EventRecorder
86 }
87
88
89
90
91 func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
92 log := r.Log.WithValues("ExternalSecret", req.NamespacedName)
93
94 resourceLabels := ctrlmetrics.RefineNonConditionMetricLabels(map[string]string{"name": req.Name, "namespace": req.Namespace})
95 start := time.Now()
96
97 syncCallsError := esmetrics.GetCounterVec(esmetrics.SyncCallsErrorKey)
98
99
100 defer func() {
101 esmetrics.GetGaugeVec(esmetrics.ExternalSecretReconcileDurationKey).With(resourceLabels).Set(float64(time.Since(start)))
102 esmetrics.GetCounterVec(esmetrics.SyncCallsKey).With(resourceLabels).Inc()
103 }()
104
105 var externalSecret esv1beta1.ExternalSecret
106 err := r.Get(ctx, req.NamespacedName, &externalSecret)
107
108 if err != nil {
109 if apierrors.IsNotFound(err) {
110 conditionSynced := NewExternalSecretCondition(esv1beta1.ExternalSecretDeleted, v1.ConditionFalse, esv1beta1.ConditionReasonSecretDeleted, "Secret was deleted")
111 SetExternalSecretCondition(&esv1beta1.ExternalSecret{
112 ObjectMeta: metav1.ObjectMeta{
113 Name: req.Name,
114 Namespace: req.Namespace,
115 },
116 }, *conditionSynced)
117
118 return ctrl.Result{}, nil
119 }
120
121 log.Error(err, errGetES)
122 syncCallsError.With(resourceLabels).Inc()
123
124 return ctrl.Result{}, err
125 }
126
127 timeSinceLastRefresh := 0 * time.Second
128 if !externalSecret.Status.RefreshTime.IsZero() {
129 timeSinceLastRefresh = time.Since(externalSecret.Status.RefreshTime.Time)
130 }
131
132
133 if externalSecret.DeletionTimestamp != nil {
134 log.Info("skipping as it is in deletion")
135 return ctrl.Result{}, nil
136 }
137
138
139 resourceLabels = ctrlmetrics.RefineLabels(resourceLabels, externalSecret.Labels)
140
141 if shouldSkipClusterSecretStore(r, externalSecret) {
142 log.Info("skipping cluster secret store as it is disabled")
143 return ctrl.Result{}, nil
144 }
145
146
147 skip, err := shouldSkipUnmanagedStore(ctx, req.Namespace, r, externalSecret)
148 if skip {
149 log.Info("skipping unmanaged store as it points to a unmanaged controllerClass")
150 return ctrl.Result{}, nil
151 }
152
153 refreshInt := r.RequeueInterval
154 if externalSecret.Spec.RefreshInterval != nil {
155 refreshInt = externalSecret.Spec.RefreshInterval.Duration
156 }
157
158
159 secretName := externalSecret.Spec.Target.Name
160 if secretName == "" {
161 secretName = externalSecret.ObjectMeta.Name
162 }
163
164
165 var existingSecret v1.Secret
166 err = r.Get(ctx, types.NamespacedName{
167 Name: secretName,
168 Namespace: externalSecret.Namespace,
169 }, &existingSecret)
170 if err != nil && !apierrors.IsNotFound(err) {
171 log.Error(err, errGetExistingSecret)
172 return ctrl.Result{}, err
173 }
174
175
176
177
178
179 if !shouldRefresh(externalSecret) && isSecretValid(existingSecret) {
180 refreshInt = (externalSecret.Spec.RefreshInterval.Duration - timeSinceLastRefresh) + 5*time.Second
181 log.V(1).Info("skipping refresh", "rv", getResourceVersion(externalSecret), "nr", refreshInt.Seconds())
182 return ctrl.Result{RequeueAfter: refreshInt}, nil
183 }
184 if !shouldReconcile(externalSecret) {
185 log.V(1).Info("stopping reconciling", "rv", getResourceVersion(externalSecret))
186 return ctrl.Result{}, nil
187 }
188
189
190 p := client.MergeFrom(externalSecret.DeepCopy())
191 defer func() {
192 err = r.Status().Patch(ctx, &externalSecret, p)
193 if err != nil {
194 log.Error(err, errPatchStatus)
195 }
196 }()
197
198 secret := &v1.Secret{
199 ObjectMeta: metav1.ObjectMeta{
200 Name: secretName,
201 Namespace: externalSecret.Namespace,
202 },
203 Immutable: &externalSecret.Spec.Target.Immutable,
204 Data: make(map[string][]byte),
205 }
206
207 dataMap, err := r.getProviderSecretData(ctx, &externalSecret)
208 if err != nil {
209 r.markAsFailed(log, errGetSecretData, err, &externalSecret, syncCallsError.With(resourceLabels))
210 return ctrl.Result{}, err
211 }
212
213
214 if len(dataMap) == 0 {
215 switch externalSecret.Spec.Target.DeletionPolicy {
216
217 case esv1beta1.DeletionPolicyDelete:
218
219
220 if externalSecret.Spec.Target.CreationPolicy != esv1beta1.CreatePolicyOwner {
221 err := fmt.Errorf(errInvalidCreatePolicy, externalSecret.Spec.Target.CreationPolicy)
222 r.markAsFailed(log, errDeleteSecret, err, &externalSecret, syncCallsError.With(resourceLabels))
223 return ctrl.Result{}, err
224 }
225
226 if err := r.Delete(ctx, secret); err != nil && !apierrors.IsNotFound(err) {
227 r.markAsFailed(log, errDeleteSecret, err, &externalSecret, syncCallsError.With(resourceLabels))
228 return ctrl.Result{}, err
229 }
230
231 conditionSynced := NewExternalSecretCondition(esv1beta1.ExternalSecretReady, v1.ConditionTrue, esv1beta1.ConditionReasonSecretDeleted, "secret deleted due to DeletionPolicy")
232 SetExternalSecretCondition(&externalSecret, *conditionSynced)
233 return ctrl.Result{RequeueAfter: refreshInt}, nil
234
235 case esv1beta1.DeletionPolicyRetain:
236 r.markAsDone(&externalSecret, start, log)
237 return ctrl.Result{RequeueAfter: refreshInt}, nil
238
239 case esv1beta1.DeletionPolicyMerge:
240 }
241 }
242
243 mutationFunc := func() error {
244 if externalSecret.Spec.Target.CreationPolicy == esv1beta1.CreatePolicyOwner {
245 err = controllerutil.SetControllerReference(&externalSecret, &secret.ObjectMeta, r.Scheme)
246 if err != nil {
247 return fmt.Errorf(errSetCtrlReference, err)
248 }
249 }
250 if secret.Data == nil {
251 secret.Data = make(map[string][]byte)
252 }
253
254 keys, err := getManagedDataKeys(&existingSecret, externalSecret.Name)
255 if err != nil {
256 return err
257 }
258
259 for _, key := range keys {
260 if dataMap[key] == nil {
261 secret.Data[key] = nil
262
263 delete(secret.Data, key)
264 }
265 }
266 err = r.applyTemplate(ctx, &externalSecret, secret, dataMap)
267 if err != nil {
268 return fmt.Errorf(errApplyTemplate, err)
269 }
270 if externalSecret.Spec.Target.CreationPolicy == esv1beta1.CreatePolicyOwner {
271 lblValue := utils.ObjectHash(fmt.Sprintf("%v/%v", externalSecret.Namespace, externalSecret.Name))
272 secret.Labels[esv1beta1.LabelOwner] = lblValue
273 }
274
275 secret.Annotations[esv1beta1.AnnotationDataHash] = r.computeDataHashAnnotation(&existingSecret, secret)
276
277 return nil
278 }
279
280 switch externalSecret.Spec.Target.CreationPolicy {
281 case esv1beta1.CreatePolicyMerge:
282 err = patchSecret(ctx, r.Client, r.Scheme, secret, mutationFunc, externalSecret.Name)
283 if err == nil {
284 externalSecret.Status.Binding = v1.LocalObjectReference{Name: secret.Name}
285 }
286 case esv1beta1.CreatePolicyNone:
287 log.V(1).Info("secret creation skipped due to creationPolicy=None")
288 err = nil
289 default:
290 var created bool
291 created, err = createOrUpdate(ctx, r.Client, secret, mutationFunc, externalSecret.Name)
292 if err == nil {
293 externalSecret.Status.Binding = v1.LocalObjectReference{Name: secret.Name}
294 }
295
296 if created {
297 delErr := deleteOrphanedSecrets(ctx, r.Client, &externalSecret)
298 if delErr != nil {
299 msg := fmt.Sprintf("failed to clean up orphaned secrets: %v", delErr)
300 r.markAsFailed(log, msg, delErr, &externalSecret, syncCallsError.With(resourceLabels))
301 return ctrl.Result{}, delErr
302 }
303 }
304 }
305
306 if err != nil {
307 r.markAsFailed(log, errUpdateSecret, err, &externalSecret, syncCallsError.With(resourceLabels))
308 return ctrl.Result{}, err
309 }
310
311 r.markAsDone(&externalSecret, start, log)
312
313 return ctrl.Result{
314 RequeueAfter: refreshInt,
315 }, nil
316 }
317
318 func (r *Reconciler) markAsDone(externalSecret *esv1beta1.ExternalSecret, start time.Time, log logr.Logger) {
319 r.recorder.Event(externalSecret, v1.EventTypeNormal, esv1beta1.ReasonUpdated, "Updated Secret")
320 conditionSynced := NewExternalSecretCondition(esv1beta1.ExternalSecretReady, v1.ConditionTrue, esv1beta1.ConditionReasonSecretSynced, "Secret was synced")
321 currCond := GetExternalSecretCondition(externalSecret.Status, esv1beta1.ExternalSecretReady)
322 SetExternalSecretCondition(externalSecret, *conditionSynced)
323 externalSecret.Status.RefreshTime = metav1.NewTime(start)
324 externalSecret.Status.SyncedResourceVersion = getResourceVersion(*externalSecret)
325 if currCond == nil || currCond.Status != conditionSynced.Status {
326 log.Info("reconciled secret")
327 } else {
328 log.V(1).Info("reconciled secret")
329 }
330 }
331
332 func (r *Reconciler) markAsFailed(log logr.Logger, msg string, err error, externalSecret *esv1beta1.ExternalSecret, counter prometheus.Counter) {
333 log.Error(err, msg)
334 r.recorder.Event(externalSecret, v1.EventTypeWarning, esv1beta1.ReasonUpdateFailed, err.Error())
335 conditionSynced := NewExternalSecretCondition(esv1beta1.ExternalSecretReady, v1.ConditionFalse, esv1beta1.ConditionReasonSecretSyncedError, msg)
336 SetExternalSecretCondition(externalSecret, *conditionSynced)
337 counter.Inc()
338 }
339
340 func deleteOrphanedSecrets(ctx context.Context, cl client.Client, externalSecret *esv1beta1.ExternalSecret) error {
341 secretList := v1.SecretList{}
342 lblValue := utils.ObjectHash(fmt.Sprintf("%v/%v", externalSecret.Namespace, externalSecret.Name))
343 ls := &metav1.LabelSelector{
344 MatchLabels: map[string]string{
345 esv1beta1.LabelOwner: lblValue,
346 },
347 }
348 labelSelector, err := metav1.LabelSelectorAsSelector(ls)
349 if err != nil {
350 return err
351 }
352 err = cl.List(ctx, &secretList, &client.ListOptions{LabelSelector: labelSelector})
353 if err != nil {
354 return err
355 }
356 for key, secret := range secretList.Items {
357 if externalSecret.Spec.Target.Name != "" && secret.Name != externalSecret.Spec.Target.Name {
358 err = cl.Delete(ctx, &secretList.Items[key])
359 if err != nil {
360 return err
361 }
362 }
363 }
364 return nil
365 }
366
367 func createOrUpdate(ctx context.Context, c client.Client, obj client.Object, f func() error, fieldOwner string) (bool, error) {
368 fqdn := fmt.Sprintf(fieldOwnerTemplate, fieldOwner)
369 key := client.ObjectKeyFromObject(obj)
370 if err := c.Get(ctx, key, obj); err != nil {
371 if !apierrors.IsNotFound(err) {
372 return false, err
373 }
374 if err := f(); err != nil {
375 return false, err
376 }
377
378 if err := c.Create(ctx, obj, client.FieldOwner(fqdn)); err != nil {
379 return false, err
380 }
381 return true, nil
382 }
383
384 existing := obj.DeepCopyObject()
385 if err := f(); err != nil {
386 return false, err
387 }
388
389 if equality.Semantic.DeepEqual(existing, obj) {
390 return false, nil
391 }
392
393 if err := c.Update(ctx, obj, client.FieldOwner(fqdn)); err != nil {
394 return false, err
395 }
396 return false, nil
397 }
398
399 func patchSecret(ctx context.Context, c client.Client, scheme *runtime.Scheme, secret *v1.Secret, mutationFunc func() error, fieldOwner string) error {
400 fqdn := fmt.Sprintf(fieldOwnerTemplate, fieldOwner)
401 err := c.Get(ctx, client.ObjectKeyFromObject(secret), secret.DeepCopy())
402 if apierrors.IsNotFound(err) {
403 return fmt.Errorf(errPolicyMergeNotFound, secret.Name)
404 }
405 if err != nil {
406 return fmt.Errorf(errPolicyMergeGetSecret, secret.Name, err)
407 }
408 existing := secret.DeepCopyObject()
409
410 err = mutationFunc()
411 if err != nil {
412 return fmt.Errorf(errPolicyMergeMutate, secret.Name, err)
413 }
414
415
416
417
418
419
420 gvks, unversioned, err := scheme.ObjectKinds(secret)
421 if err != nil {
422 return err
423 }
424 if !unversioned && len(gvks) == 1 {
425 secret.SetGroupVersionKind(gvks[0])
426 }
427
428 if equality.Semantic.DeepEqual(existing, secret) {
429 return nil
430 }
431
432 secret.ObjectMeta.ManagedFields = nil
433
434
435 err = c.Patch(ctx, secret, client.Apply, client.FieldOwner(fqdn), client.ForceOwnership)
436 if err != nil {
437 return fmt.Errorf(errPolicyMergePatch, secret.Name, err)
438 }
439 return nil
440 }
441
442 func getManagedDataKeys(secret *v1.Secret, fieldOwner string) ([]string, error) {
443 return getManagedFieldKeys(secret, fieldOwner, func(fields map[string]interface{}) []string {
444 dataFields := fields["f:data"]
445 if dataFields == nil {
446 return nil
447 }
448 df, ok := dataFields.(map[string]interface{})
449 if !ok {
450 return nil
451 }
452 var keys []string
453 for k := range df {
454 keys = append(keys, k)
455 }
456 return keys
457 })
458 }
459
460 func getManagedFieldKeys(
461 secret *v1.Secret,
462 fieldOwner string,
463 process func(fields map[string]interface{}) []string,
464 ) ([]string, error) {
465 fqdn := fmt.Sprintf(fieldOwnerTemplate, fieldOwner)
466 var keys []string
467 for _, v := range secret.ObjectMeta.ManagedFields {
468 if v.Manager != fqdn {
469 continue
470 }
471 fields := make(map[string]interface{})
472 err := json.Unmarshal(v.FieldsV1.Raw, &fields)
473 if err != nil {
474 return nil, fmt.Errorf("error unmarshaling managed fields: %w", err)
475 }
476 for _, key := range process(fields) {
477 if key == "." {
478 continue
479 }
480 keys = append(keys, strings.TrimPrefix(key, "f:"))
481 }
482 }
483 return keys, nil
484 }
485
486 func getResourceVersion(es esv1beta1.ExternalSecret) string {
487 return fmt.Sprintf("%d-%s", es.ObjectMeta.GetGeneration(), hashMeta(es.ObjectMeta))
488 }
489
490 func hashMeta(m metav1.ObjectMeta) string {
491 type meta struct {
492 annotations map[string]string
493 labels map[string]string
494 }
495 return utils.ObjectHash(meta{
496 annotations: m.Annotations,
497 labels: m.Labels,
498 })
499 }
500
501 func shouldSkipClusterSecretStore(r *Reconciler, es esv1beta1.ExternalSecret) bool {
502 return !r.ClusterSecretStoreEnabled && es.Spec.SecretStoreRef.Kind == esv1beta1.ClusterSecretStoreKind
503 }
504
505
506
507
508 func shouldSkipUnmanagedStore(ctx context.Context, namespace string, r *Reconciler, es esv1beta1.ExternalSecret) (bool, error) {
509 var storeList []esv1beta1.SecretStoreRef
510
511 if es.Spec.SecretStoreRef.Name != "" {
512 storeList = append(storeList, es.Spec.SecretStoreRef)
513 }
514
515 for _, ref := range es.Spec.Data {
516 if ref.SourceRef != nil {
517 storeList = append(storeList, ref.SourceRef.SecretStoreRef)
518 }
519 }
520
521 for _, ref := range es.Spec.DataFrom {
522 if ref.SourceRef != nil && ref.SourceRef.SecretStoreRef != nil {
523 storeList = append(storeList, *ref.SourceRef.SecretStoreRef)
524 }
525
526
527 if ref.SourceRef != nil && ref.SourceRef.GeneratorRef != nil {
528 genDef, err := r.getGeneratorDefinition(ctx, namespace, ref.SourceRef.GeneratorRef)
529 if err != nil {
530 return false, err
531 }
532 skipGenerator, err := shouldSkipGenerator(r, genDef)
533 if err != nil {
534 return false, err
535 }
536 if skipGenerator {
537 return true, nil
538 }
539 }
540 }
541
542 for _, ref := range storeList {
543 var store esv1beta1.GenericStore
544
545 switch ref.Kind {
546 case esv1beta1.SecretStoreKind, "":
547 store = &esv1beta1.SecretStore{}
548 case esv1beta1.ClusterSecretStoreKind:
549 store = &esv1beta1.ClusterSecretStore{}
550 namespace = ""
551 }
552
553 err := r.Client.Get(ctx, types.NamespacedName{
554 Name: ref.Name,
555 Namespace: namespace,
556 }, store)
557 if err != nil {
558 return false, err
559 }
560 class := store.GetSpec().Controller
561 if class != "" && class != r.ControllerClass {
562 return true, nil
563 }
564 }
565 return false, nil
566 }
567
568 func shouldRefresh(es esv1beta1.ExternalSecret) bool {
569
570 if es.Status.SyncedResourceVersion != getResourceVersion(es) {
571 return true
572 }
573
574
575 if es.Spec.RefreshInterval.Duration == 0 && es.Status.SyncedResourceVersion != "" {
576 return false
577 }
578 if es.Status.RefreshTime.IsZero() {
579 return true
580 }
581 return es.Status.RefreshTime.Add(es.Spec.RefreshInterval.Duration).Before(time.Now())
582 }
583
584 func shouldReconcile(es esv1beta1.ExternalSecret) bool {
585 if es.Spec.Target.Immutable && hasSyncedCondition(es) {
586 return false
587 }
588 return true
589 }
590
591 func hasSyncedCondition(es esv1beta1.ExternalSecret) bool {
592 for _, condition := range es.Status.Conditions {
593 if condition.Reason == "SecretSynced" {
594 return true
595 }
596 }
597 return false
598 }
599
600
601 func isSecretValid(existingSecret v1.Secret) bool {
602
603 if existingSecret.UID == "" || existingSecret.Annotations == nil {
604 return false
605 }
606
607
608 if existingSecret.Annotations[esv1beta1.AnnotationDataHash] != utils.ObjectHash(existingSecret.Data) {
609 return false
610 }
611 return true
612 }
613
614
615 func (r *Reconciler) computeDataHashAnnotation(existing, secret *v1.Secret) string {
616 data := make(map[string][]byte)
617 for k, v := range existing.Data {
618 data[k] = v
619 }
620 for k, v := range secret.Data {
621 data[k] = v
622 }
623 return utils.ObjectHash(data)
624 }
625
626
627 func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
628 r.recorder = mgr.GetEventRecorderFor("external-secrets")
629
630 return ctrl.NewControllerManagedBy(mgr).
631 WithOptions(opts).
632 For(&esv1beta1.ExternalSecret{}).
633 Owns(&v1.Secret{}, builder.OnlyMetadata).
634 Complete(r)
635 }
636
View as plain text