| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- /*
- Copyright © The ESO Authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- https://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package externalsecret
- import (
- "context"
- "fmt"
- "maps"
- "github.com/go-logr/logr"
- v1 "k8s.io/api/core/v1"
- apierrors "k8s.io/apimachinery/pkg/api/errors"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
- esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
- "github.com/external-secrets/external-secrets/pkg/controllers/templating"
- "github.com/external-secrets/external-secrets/runtime/esutils"
- "github.com/external-secrets/external-secrets/runtime/template"
- )
- // isGenericTarget checks if the ExternalSecret targets a generic resource.
- func isGenericTarget(es *esv1.ExternalSecret) bool {
- return es.Spec.Target.Manifest != nil
- }
- // validateGenericTarget validates that generic targets are properly configured.
- func (r *Reconciler) validateGenericTarget(log logr.Logger, es *esv1.ExternalSecret) error {
- if !r.AllowGenericTargets {
- return fmt.Errorf("generic targets are disabled. Enable with --unsafe-allow-generic-targets flag")
- }
- manifest := es.Spec.Target.Manifest
- if manifest.APIVersion == "" {
- return fmt.Errorf("target.manifest.apiVersion is required")
- }
- if manifest.Kind == "" {
- return fmt.Errorf("target.manifest.kind is required")
- }
- log.Info("Warning: Using generic target. Make sure access policies and encryption are properly configured.",
- "apiVersion", manifest.APIVersion,
- "kind", manifest.Kind,
- "name", getTargetName(es))
- return nil
- }
- // getTargetGVK returns the GroupVersionKind for the target resource.
- func getTargetGVK(es *esv1.ExternalSecret) schema.GroupVersionKind {
- manifest := es.Spec.Target.Manifest
- gv, _ := schema.ParseGroupVersion(manifest.APIVersion)
- return schema.GroupVersionKind{
- Group: gv.Group,
- Version: gv.Version,
- Kind: manifest.Kind,
- }
- }
- // getTargetName returns the name of the target resource.
- func getTargetName(es *esv1.ExternalSecret) string {
- if es.Spec.Target.Name != "" {
- return es.Spec.Target.Name
- }
- return es.Name
- }
- // getGenericResource retrieves a generic resource using the controller-runtime client.
- func (r *Reconciler) getGenericResource(ctx context.Context, log logr.Logger, es *esv1.ExternalSecret) (*unstructured.Unstructured, error) {
- gvk := getTargetGVK(es)
- resource := &unstructured.Unstructured{}
- resource.SetGroupVersionKind(gvk)
- err := r.Client.Get(ctx, client.ObjectKey{
- Namespace: es.Namespace,
- Name: getTargetName(es),
- }, resource)
- if err != nil {
- if apierrors.IsNotFound(err) {
- log.V(1).Info("target resource does not exist", "gvk", gvk.String(), "name", getTargetName(es))
- return nil, err
- }
- return nil, fmt.Errorf("failed to get target resource: %w", err)
- }
- return resource, nil
- }
- func (r *Reconciler) createGenericResource(ctx context.Context, log logr.Logger, es *esv1.ExternalSecret, obj *unstructured.Unstructured) error {
- gvk := getTargetGVK(es)
- // Check if resource already exists
- existing := &unstructured.Unstructured{}
- existing.SetGroupVersionKind(gvk)
- err := r.Client.Get(ctx, client.ObjectKey{
- Namespace: es.Namespace,
- Name: getTargetName(es),
- }, existing)
- if err != nil {
- if !apierrors.IsNotFound(err) {
- return fmt.Errorf("failed to check if target resource exists: %w", err)
- }
- } else {
- return fmt.Errorf("target resource with name %s already exists", getTargetName(es))
- }
- log.Info("creating target resource", "gvk", gvk.String(), "name", getTargetName(es))
- err = r.Client.Create(ctx, obj)
- if err != nil {
- return fmt.Errorf("failed to create target resource: %w", err)
- }
- r.recorder.Event(es, v1.EventTypeNormal, "Created", fmt.Sprintf("Created %s %s", gvk.Kind, getTargetName(es)))
- return nil
- }
- func (r *Reconciler) updateGenericResource(ctx context.Context, log logr.Logger, es *esv1.ExternalSecret, existing *unstructured.Unstructured) error {
- gvk := getTargetGVK(es)
- log.Info("updating target resource", "gvk", gvk.String(), "name", getTargetName(es))
- err := r.Client.Update(ctx, existing)
- if err != nil {
- return fmt.Errorf("failed to update target resource: %w", err)
- }
- r.recorder.Event(es, v1.EventTypeNormal, "Updated", fmt.Sprintf("Updated %s %s", gvk.Kind, getTargetName(es)))
- return nil
- }
- // deleteGenericResource deletes a generic resource.
- func (r *Reconciler) deleteGenericResource(ctx context.Context, log logr.Logger, es *esv1.ExternalSecret) error {
- if !r.AllowGenericTargets || !isGenericTarget(es) {
- return nil
- }
- gvk := getTargetGVK(es)
- obj := &unstructured.Unstructured{}
- obj.SetGroupVersionKind(gvk)
- obj.SetNamespace(es.Namespace)
- obj.SetName(getTargetName(es))
- log.Info("deleting target resource", "gvk", gvk.String(), "name", getTargetName(es))
- err := r.Client.Delete(ctx, obj)
- if err != nil && !apierrors.IsNotFound(err) {
- return fmt.Errorf("failed to delete target resource: %w", err)
- }
- r.recorder.Event(es, v1.EventTypeNormal, "Deleted", fmt.Sprintf("Deleted %s %s", gvk.Kind, getTargetName(es)))
- return nil
- }
- // applyTemplateToManifest renders templates for generic resources and returns an unstructured object.
- // If existingObj is provided, templates will be applied to it (for merge behavior).
- // Otherwise, a new object is created.
- func (r *Reconciler) applyTemplateToManifest(ctx context.Context, es *esv1.ExternalSecret, dataMap map[string][]byte, existingObj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
- var obj *unstructured.Unstructured
- if existingObj != nil {
- // use the existing object for merge behavior if it exists
- obj = existingObj.DeepCopy()
- } else {
- gvk := getTargetGVK(es)
- obj = &unstructured.Unstructured{}
- obj.SetGroupVersionKind(gvk)
- obj.SetName(getTargetName(es))
- obj.SetNamespace(es.Namespace)
- switch gvk.Kind {
- case "ConfigMap", "Secret":
- obj.Object["data"] = map[string]any{}
- default:
- obj.Object["spec"] = map[string]any{}
- }
- }
- labels := obj.GetLabels()
- if labels == nil {
- labels = make(map[string]string)
- }
- annotations := obj.GetAnnotations()
- if annotations == nil {
- annotations = make(map[string]string)
- }
- srcLabels, srcAnnotations := es.ObjectMeta.Labels, es.ObjectMeta.Annotations
- if es.Spec.Target.Template != nil {
- srcLabels = es.Spec.Target.Template.Metadata.Labels
- srcAnnotations = es.Spec.Target.Template.Metadata.Annotations
- }
- maps.Copy(labels, srcLabels)
- maps.Copy(annotations, srcAnnotations)
- labels[esv1.LabelManaged] = esv1.LabelManagedValue
- obj.SetLabels(labels)
- obj.SetAnnotations(annotations)
- var result *unstructured.Unstructured
- var err error
- if es.Spec.Target.Template == nil {
- result = r.createSimpleManifest(obj, dataMap)
- } else {
- result, err = r.renderTemplatedManifest(ctx, es, obj, dataMap)
- }
- if err != nil {
- return nil, err
- }
- ann := result.GetAnnotations()
- if ann == nil {
- ann = make(map[string]string)
- }
- hash, err := genericTargetContentHash(result)
- if err != nil {
- return nil, fmt.Errorf("failed to hash target %q content: %w", es.Spec.Target.Name, err)
- }
- ann[esv1.AnnotationDataHash] = hash
- result.SetAnnotations(ann)
- if err := r.applyOwnership(es, result); err != nil {
- return nil, err
- }
- return result, nil
- }
- // applyOwnership manages the owner reference and owner label on the target manifest resource.
- func (r *Reconciler) applyOwnership(es *esv1.ExternalSecret, result *unstructured.Unstructured) error {
- // get information about the current owner of the resource
- // - we ignore the API version as it can change over time
- // - we ignore the UID for consistency with the SetControllerReference function
- currentOwner := metav1.GetControllerOf(result)
- ownerIsESKind := false
- ownerIsCurrentES := false
- if currentOwner != nil {
- currentOwnerGK := schema.FromAPIVersionAndKind(currentOwner.APIVersion, currentOwner.Kind).GroupKind()
- ownerIsESKind = currentOwnerGK.String() == esv1.ExtSecretGroupKind
- ownerIsCurrentES = ownerIsESKind && currentOwner.Name == es.Name
- }
- // if another ExternalSecret is the owner, we should return an error
- // otherwise the controller will fight with itself to update the resource.
- // note, this does not prevent other controllers from owning the resource.
- if ownerIsESKind && !ownerIsCurrentES {
- return fmt.Errorf("%w: %s", ErrSecretIsOwned, currentOwner.Name)
- }
- // if the CreationPolicy is Owner, we should set ourselves as the owner of the resource
- if es.Spec.Target.CreationPolicy == esv1.CreatePolicyOwner {
- if err := controllerutil.SetControllerReference(es, result, r.Scheme); err != nil {
- return fmt.Errorf("%w: %w", ErrSecretSetCtrlRef, err)
- }
- }
- // if the creation policy is not Owner, we should remove ourselves as the owner
- // this could happen if the creation policy was changed after the resource was created
- if es.Spec.Target.CreationPolicy != esv1.CreatePolicyOwner && ownerIsCurrentES {
- if err := controllerutil.RemoveControllerReference(es, result, r.Scheme); err != nil {
- return fmt.Errorf("%w: %w", ErrSecretRemoveCtrlRef, err)
- }
- }
- labels := result.GetLabels()
- if labels == nil {
- labels = make(map[string]string)
- }
- // we also use a label to keep track of the owner of the resource
- // this lets us remove resources that are no longer needed if the target name changes
- // the label should not be set if the creation policy is not Owner
- if es.Spec.Target.CreationPolicy == esv1.CreatePolicyOwner {
- labels[esv1.LabelOwner] = esutils.ObjectHash(fmt.Sprintf("%v/%v", es.Namespace, es.Name))
- } else {
- delete(labels, esv1.LabelOwner)
- }
- result.SetLabels(labels)
- return nil
- }
- // createSimpleManifest creates a simple resource without templates (e.g., ConfigMap with data field).
- func (r *Reconciler) createSimpleManifest(obj *unstructured.Unstructured, dataMap map[string][]byte) *unstructured.Unstructured {
- // For ConfigMaps and similar resources, put data in .data field
- if obj.GetKind() == "ConfigMap" {
- data := make(map[string]string)
- for k, v := range dataMap {
- data[k] = string(v)
- }
- obj.Object["data"] = data
- return obj
- }
- // For other resources, put in spec.data or just data
- data := make(map[string]string)
- for k, v := range dataMap {
- data[k] = string(v)
- }
- if obj.Object["spec"] == nil {
- obj.Object["spec"] = make(map[string]any)
- }
- spec := obj.Object["spec"].(map[string]any)
- spec["data"] = data
- return obj
- }
- // renderTemplatedManifest renders templates for a custom resource.
- func (r *Reconciler) renderTemplatedManifest(ctx context.Context, es *esv1.ExternalSecret, obj *unstructured.Unstructured, dataMap map[string][]byte) (*unstructured.Unstructured, error) {
- execute, err := template.EngineForVersion(es.Spec.Target.Template.EngineVersion)
- if err != nil {
- return nil, fmt.Errorf("failed to get template engine: %w", err)
- }
- // Handle templateFrom entries
- for _, tplFrom := range es.Spec.Target.Template.TemplateFrom {
- targetPath := tplFrom.Target
- if targetPath == "" {
- targetPath = esv1.TemplateTargetData
- }
- if tplFrom.Literal != nil {
- // Execute template directly against the unstructured object
- out := make(map[string][]byte)
- out[*tplFrom.Literal] = []byte(*tplFrom.Literal)
- if err := execute(out, dataMap, esv1.TemplateScopeKeysAndValues, targetPath, obj); err != nil {
- return nil, fmt.Errorf("failed to execute literal template: %w", err)
- }
- }
- if tplFrom.ConfigMap != nil || tplFrom.Secret != nil {
- // Parser still uses v1.Secret, so collect data and apply via template engine to the end result.
- tempSecret := &v1.Secret{Data: make(map[string][]byte)}
- p := templating.Parser{
- Client: r.Client,
- TargetSecret: tempSecret,
- DataMap: dataMap,
- Exec: execute,
- }
- if tplFrom.ConfigMap != nil {
- if err := p.MergeConfigMap(ctx, es.Namespace, tplFrom); err != nil {
- return nil, fmt.Errorf("failed to merge configmap template: %w", err)
- }
- }
- if tplFrom.Secret != nil {
- if err := p.MergeSecret(ctx, es.Namespace, tplFrom); err != nil {
- return nil, fmt.Errorf("failed to merge secret template: %w", err)
- }
- }
- // apply collected data to the target object
- if err := execute(tempSecret.Data, dataMap, esv1.TemplateScopeValues, targetPath, obj); err != nil {
- return nil, fmt.Errorf("failed to apply merged templates to path %s: %w", targetPath, err)
- }
- }
- }
- // Handle template.data entries
- if len(es.Spec.Target.Template.Data) > 0 {
- tplMap := make(map[string][]byte)
- for k, v := range es.Spec.Target.Template.Data {
- tplMap[k] = []byte(v)
- }
- if err := execute(tplMap, dataMap, esv1.TemplateScopeValues, esv1.TemplateTargetData, obj); err != nil {
- return nil, fmt.Errorf("failed to execute template.data: %w", err)
- }
- }
- return obj, nil
- }
|