diff --git a/apis/externalsecrets/v1alpha1/pushsecret_types.go b/apis/externalsecrets/v1alpha1/pushsecret_types.go index 3f85a311c..3888534a1 100644 --- a/apis/externalsecrets/v1alpha1/pushsecret_types.go +++ b/apis/externalsecrets/v1alpha1/pushsecret_types.go @@ -92,9 +92,16 @@ type PushSecretSecret struct { Name string `json:"name"` } +// +kubebuilder:validation:MinProperties=1 +// +kubebuilder:validation:MaxProperties=1 type PushSecretSelector struct { // Select a Secret to Push. - Secret PushSecretSecret `json:"secret"` + // +optional + Secret *PushSecretSecret `json:"secret,omitempty"` + + // Point to a generator to create a Secret. + // +optional + GeneratorRef *esv1beta1.GeneratorRef `json:"generatorRef,omitempty"` } type PushSecretRemoteRef struct { diff --git a/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go b/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go index d7f0e66d4..da4595be3 100644 --- a/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go +++ b/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go @@ -1208,7 +1208,16 @@ func (in *PushSecretSecret) DeepCopy() *PushSecretSecret { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PushSecretSelector) DeepCopyInto(out *PushSecretSelector) { *out = *in - out.Secret = in.Secret + if in.Secret != nil { + in, out := &in.Secret, &out.Secret + *out = new(PushSecretSecret) + **out = **in + } + if in.GeneratorRef != nil { + in, out := &in.GeneratorRef, &out.GeneratorRef + *out = new(v1beta1.GeneratorRef) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretSelector. @@ -1236,7 +1245,7 @@ func (in *PushSecretSpec) DeepCopyInto(out *PushSecretSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - out.Selector = in.Selector + in.Selector.DeepCopyInto(&out.Selector) if in.Data != nil { in, out := &in.Data, &out.Data *out = make([]PushSecretData, len(*in)) diff --git a/cmd/root.go b/cmd/root.go index 6f77c7bbc..0472d6650 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -216,6 +216,7 @@ var rootCmd = &cobra.Command{ Log: ctrl.Log.WithName("controllers").WithName("PushSecret"), Scheme: mgr.GetScheme(), ControllerClass: controllerClass, + RestConfig: mgr.GetConfig(), RequeueInterval: time.Hour, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, errCreateController, "controller", "PushSecret") diff --git a/config/crds/bases/external-secrets.io_pushsecrets.yaml b/config/crds/bases/external-secrets.io_pushsecrets.yaml index 0322bfae5..5e59a58bd 100644 --- a/config/crds/bases/external-secrets.io_pushsecrets.yaml +++ b/config/crds/bases/external-secrets.io_pushsecrets.yaml @@ -165,7 +165,27 @@ spec: type: array selector: description: The Secret Selector (k8s source) for the Push Secret + maxProperties: 1 + minProperties: 1 properties: + generatorRef: + description: Point to a generator to create a Secret. + properties: + apiVersion: + default: generators.external-secrets.io/v1alpha1 + description: Specify the apiVersion of the generator resource + type: string + kind: + description: Specify the Kind of the resource, e.g. Password, + ACRAccessToken etc. + type: string + name: + description: Specify the name of the generator resource + type: string + required: + - kind + - name + type: object secret: description: Select a Secret to Push. properties: @@ -176,8 +196,6 @@ spec: required: - name type: object - required: - - secret type: object template: description: Template defines a blueprint for the created Secret resource. diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml index e4fb22963..65bea24d2 100644 --- a/deploy/crds/bundle.yaml +++ b/deploy/crds/bundle.yaml @@ -6258,7 +6258,26 @@ spec: type: array selector: description: The Secret Selector (k8s source) for the Push Secret + maxProperties: 1 + minProperties: 1 properties: + generatorRef: + description: Point to a generator to create a Secret. + properties: + apiVersion: + default: generators.external-secrets.io/v1alpha1 + description: Specify the apiVersion of the generator resource + type: string + kind: + description: Specify the Kind of the resource, e.g. Password, ACRAccessToken etc. + type: string + name: + description: Specify the name of the generator resource + type: string + required: + - kind + - name + type: object secret: description: Select a Secret to Push. properties: @@ -6268,8 +6287,6 @@ spec: required: - name type: object - required: - - secret type: object template: description: Template defines a blueprint for the created Secret resource. diff --git a/docs/guides/pushsecrets.md b/docs/guides/pushsecrets.md index dfb34b61e..dd6714f8f 100644 --- a/docs/guides/pushsecrets.md +++ b/docs/guides/pushsecrets.md @@ -43,3 +43,12 @@ This will _marshal_ the entire secret data and push it into this single property ### Key conversion strategy You can also set `data[*].conversionStrategy: ReverseUnicode` to reverse the invalid character replaced by the `conversionStrategy: Unicode` configuration in the `ExternalSecret` object as [documented here](../guides/getallsecrets.md#avoiding-name-conflicts). + +## Rotate Secrets + +You can use ESO to rotate secrets by using the PushSecret and Generator resources. ESO will consult the `Kind=Generator` to generate a new secret and then ESO will store it. +Every `spec.refreshInterval` the secret will be rotated and the value will be replaced in the store unless `spec.updatePolicy=IfNotExist` is set. Then ESO will generate the secret once and won't rotate it. + +```yaml +{% include 'pushsecret-generator-rotation-example.yaml' %} +``` diff --git a/docs/snippets/full-pushsecret.yaml b/docs/snippets/full-pushsecret.yaml index 7b5a3c87c..f8ff45f52 100644 --- a/docs/snippets/full-pushsecret.yaml +++ b/docs/snippets/full-pushsecret.yaml @@ -14,6 +14,11 @@ spec: selector: secret: name: pokedex-credentials # Source Kubernetes secret to be pushed + # Alternatively, you can point to a generator that produces values to be pushed + generatorRef: + apiVersion: external-secrets.io/v1alpha1 + kind: ECRAuthorizationToken + name: prod-registry-credentials template: metadata: annotations: { } diff --git a/docs/snippets/pushsecret-generator-rotation-example.yaml b/docs/snippets/pushsecret-generator-rotation-example.yaml new file mode 100644 index 000000000..8bf62c75f --- /dev/null +++ b/docs/snippets/pushsecret-generator-rotation-example.yaml @@ -0,0 +1,33 @@ +{% raw %} +apiVersion: generators.external-secrets.io/v1alpha1 +kind: Password +metadata: + name: strong-password +spec: + length: 128 + digits: 5 + symbols: 5 + symbolCharacters: "-_$@" + noUpper: false + allowRepeat: true +--- +apiVersion: external-secrets.io/v1alpha1 +kind: PushSecret +metadata: + name: pushsecret-example +spec: + refreshInterval: 6h + secretStoreRefs: + - name: aws-parameter-store + kind: SecretStore + selector: + generatorRef: + apiVersion: generators.external-secrets.io/v1alpha1 + kind: Password + name: strong-password + data: + - match: + secretKey: password # property in the generator output + remoteRef: + remoteKey: prod/myql/password +{% endraw %} diff --git a/e2e/suites/provider/cases/template/template.go b/e2e/suites/provider/cases/template/template.go index 3c60ec459..a2819b10e 100644 --- a/e2e/suites/provider/cases/template/template.go +++ b/e2e/suites/provider/cases/template/template.go @@ -133,7 +133,7 @@ func genericPushSecretTemplate(f *framework.Framework) (string, func(*framework. Type: v1.SecretTypeOpaque, } tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{ - Secret: esv1alpha1.PushSecretSecret{ + Secret: &esv1alpha1.PushSecretSecret{ Name: secretKey1, }, } diff --git a/pkg/controllers/externalsecret/externalsecret_controller.go b/pkg/controllers/externalsecret/externalsecret_controller.go index 9be861a25..c800a9bd0 100644 --- a/pkg/controllers/externalsecret/externalsecret_controller.go +++ b/pkg/controllers/externalsecret/externalsecret_controller.go @@ -48,6 +48,7 @@ import ( "github.com/external-secrets/external-secrets/pkg/controllers/externalsecret/esmetrics" ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics" "github.com/external-secrets/external-secrets/pkg/utils" + "github.com/external-secrets/external-secrets/pkg/utils/resolvers" // Loading registered generators. _ "github.com/external-secrets/external-secrets/pkg/generator/register" @@ -549,11 +550,11 @@ func shouldSkipUnmanagedStore(ctx context.Context, namespace string, r *Reconcil // verify that generator's controllerClass matches if ref.SourceRef != nil && ref.SourceRef.GeneratorRef != nil { - genDef, err := r.getGeneratorDefinition(ctx, namespace, ref.SourceRef.GeneratorRef) + _, obj, err := resolvers.GeneratorRef(ctx, r.RestConfig, namespace, ref.SourceRef.GeneratorRef) if err != nil { return false, err } - skipGenerator, err := shouldSkipGenerator(r, genDef) + skipGenerator, err := shouldSkipGenerator(r, obj) if err != nil { return false, err } diff --git a/pkg/controllers/externalsecret/externalsecret_controller_secret.go b/pkg/controllers/externalsecret/externalsecret_controller_secret.go index 119e73162..fd2318613 100644 --- a/pkg/controllers/externalsecret/externalsecret_controller_secret.go +++ b/pkg/controllers/externalsecret/externalsecret_controller_secret.go @@ -22,17 +22,13 @@ import ( v1 "k8s.io/api/core/v1" apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/restmapper" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1" // Loading registered providers. "github.com/external-secrets/external-secrets/pkg/controllers/secretstore" "github.com/external-secrets/external-secrets/pkg/utils" + "github.com/external-secrets/external-secrets/pkg/utils/resolvers" // Loading registered generators. _ "github.com/external-secrets/external-secrets/pkg/generator/register" @@ -116,15 +112,11 @@ func toStoreGenSourceRef(ref *esv1beta1.StoreSourceRef) *esv1beta1.StoreGenerato } func (r *Reconciler) handleGenerateSecrets(ctx context.Context, namespace string, remoteRef esv1beta1.ExternalSecretDataFromRemoteRef, i int) (map[string][]byte, error) { - genDef, err := r.getGeneratorDefinition(ctx, namespace, remoteRef.SourceRef.GeneratorRef) + gen, obj, err := resolvers.GeneratorRef(ctx, r.RestConfig, namespace, remoteRef.SourceRef.GeneratorRef) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to resolve generator: %w", err) } - gen, err := genv1alpha1.GetGenerator(genDef) - if err != nil { - return nil, err - } - secretMap, err := gen.Generate(ctx, genDef, r.Client, namespace) + secretMap, err := gen.Generate(ctx, obj, r.Client, namespace) if err != nil { return nil, fmt.Errorf(errGenerate, i, err) } @@ -138,49 +130,6 @@ func (r *Reconciler) handleGenerateSecrets(ctx context.Context, namespace string return secretMap, err } -// getGeneratorDefinition returns the generator JSON for a given sourceRef -// when it uses a generatorRef it fetches the resource and returns the JSON. -func (r *Reconciler) getGeneratorDefinition(ctx context.Context, namespace string, generatorRef *esv1beta1.GeneratorRef) (*apiextensions.JSON, error) { - // client-go dynamic client needs a GVR to fetch the resource - // But we only have the GVK in our generatorRef. - // - // TODO: there is no need to discover the GroupVersionResource - // this should be cached. - c := discovery.NewDiscoveryClientForConfigOrDie(r.RestConfig) - groupResources, err := restmapper.GetAPIGroupResources(c) - if err != nil { - return nil, err - } - - gv, err := schema.ParseGroupVersion(generatorRef.APIVersion) - if err != nil { - return nil, err - } - mapper := restmapper.NewDiscoveryRESTMapper(groupResources) - mapping, err := mapper.RESTMapping(schema.GroupKind{ - Group: gv.Group, - Kind: generatorRef.Kind, - }) - if err != nil { - return nil, err - } - d, err := dynamic.NewForConfig(r.RestConfig) - if err != nil { - return nil, err - } - res, err := d.Resource(mapping.Resource). - Namespace(namespace). - Get(ctx, generatorRef.Name, metav1.GetOptions{}) - if err != nil { - return nil, err - } - jsonRes, err := res.MarshalJSON() - if err != nil { - return nil, err - } - return &apiextensions.JSON{Raw: jsonRes}, nil -} - func (r *Reconciler) handleExtractSecrets(ctx context.Context, externalSecret *esv1beta1.ExternalSecret, remoteRef esv1beta1.ExternalSecretDataFromRemoteRef, cmgr *secretstore.Manager, i int) (map[string][]byte, error) { client, err := cmgr.Get(ctx, externalSecret.Spec.SecretStoreRef, externalSecret.Namespace, remoteRef.SourceRef) if err != nil { diff --git a/pkg/controllers/pushsecret/pushsecret_controller.go b/pkg/controllers/pushsecret/pushsecret_controller.go index 90db94726..d30f49666 100644 --- a/pkg/controllers/pushsecret/pushsecret_controller.go +++ b/pkg/controllers/pushsecret/pushsecret_controller.go @@ -28,6 +28,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -40,6 +41,10 @@ import ( "github.com/external-secrets/external-secrets/pkg/controllers/secretstore" "github.com/external-secrets/external-secrets/pkg/provider/util/locks" "github.com/external-secrets/external-secrets/pkg/utils" + "github.com/external-secrets/external-secrets/pkg/utils/resolvers" + + // load generators. + _ "github.com/external-secrets/external-secrets/pkg/generator/register" ) const ( @@ -59,6 +64,7 @@ type Reconciler struct { Log logr.Logger Scheme *runtime.Scheme recorder record.EventRecorder + RestConfig *rest.Config RequeueInterval time.Duration ControllerClass string } @@ -148,7 +154,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu default: } - secret, err := r.GetSecret(ctx, ps) + secret, err := r.resolveSecret(ctx, ps) if err != nil { r.markAsFailed(errFailedGetSecret, &ps, nil) @@ -347,14 +353,38 @@ func secretKeyExists(key string, secret *v1.Secret) bool { return key == "" || ok } -func (r *Reconciler) GetSecret(ctx context.Context, ps esapi.PushSecret) (*v1.Secret, error) { - secretName := types.NamespacedName{Name: ps.Spec.Selector.Secret.Name, Namespace: ps.Namespace} - secret := &v1.Secret{} - err := r.Client.Get(ctx, secretName, secret) - if err != nil { - return nil, err +func (r *Reconciler) resolveSecret(ctx context.Context, ps esapi.PushSecret) (*v1.Secret, error) { + if ps.Spec.Selector.Secret != nil { + secretName := types.NamespacedName{Name: ps.Spec.Selector.Secret.Name, Namespace: ps.Namespace} + secret := &v1.Secret{} + err := r.Client.Get(ctx, secretName, secret) + if err != nil { + return nil, err + } + return secret, nil } - return secret, nil + if ps.Spec.Selector.GeneratorRef != nil { + return r.resolveSecretFromGenerator(ctx, ps.Namespace, ps.Spec.Selector.GeneratorRef) + } + return nil, errors.New("no secret selector provided") +} + +func (r *Reconciler) resolveSecretFromGenerator(ctx context.Context, namespace string, generatorRef *v1beta1.GeneratorRef) (*v1.Secret, error) { + gen, obj, err := resolvers.GeneratorRef(ctx, r.RestConfig, namespace, generatorRef) + if err != nil { + return nil, fmt.Errorf("unable to resolve generator: %w", err) + } + secretMap, err := gen.Generate(ctx, obj, r.Client, namespace) + if err != nil { + return nil, fmt.Errorf("unable to generate: %w", err) + } + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "___generated-secret", + Namespace: namespace, + }, + Data: secretMap, + }, err } func (r *Reconciler) GetSecretStores(ctx context.Context, ps esapi.PushSecret) (map[esapi.PushSecretStoreRef]v1beta1.GenericStore, error) { diff --git a/pkg/controllers/pushsecret/pushsecret_controller_test.go b/pkg/controllers/pushsecret/pushsecret_controller_test.go index c7e59baa0..7c153cbc6 100644 --- a/pkg/controllers/pushsecret/pushsecret_controller_test.go +++ b/pkg/controllers/pushsecret/pushsecret_controller_test.go @@ -30,6 +30,7 @@ import ( "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" + genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1" ctest "github.com/external-secrets/external-secrets/pkg/controllers/commontest" "github.com/external-secrets/external-secrets/pkg/controllers/pushsecret/psmetrics" "github.com/external-secrets/external-secrets/pkg/provider/testing/fake" @@ -99,6 +100,21 @@ var _ = Describe("PushSecret controller", func() { PushSecretNamespace, err = ctest.CreateNamespace("test-ns", k8sClient) Expect(err).ToNot(HaveOccurred()) fakeProvider.Reset() + + Expect(k8sClient.Create(context.Background(), &genv1alpha1.Fake{ + TypeMeta: metav1.TypeMeta{ + Kind: "Fake", + APIVersion: "generators.external-secrets.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: PushSecretNamespace, + }, + Spec: genv1alpha1.FakeSpec{ + Data: map[string]string{ + "key": "foo-bar-from-generator", + }, + }})).ToNot(HaveOccurred()) }) AfterEach(func() { @@ -162,7 +178,7 @@ var _ = Describe("PushSecret controller", func() { }, }, Selector: v1alpha1.PushSecretSelector{ - Secret: v1alpha1.PushSecretSecret{ + Secret: &v1alpha1.PushSecretSecret{ Name: SecretName, }, }, @@ -395,7 +411,7 @@ var _ = Describe("PushSecret controller", func() { }, }, Selector: v1alpha1.PushSecretSelector{ - Secret: v1alpha1.PushSecretSecret{ + Secret: &v1alpha1.PushSecretSecret{ Name: SecretName, }, }, @@ -459,7 +475,7 @@ var _ = Describe("PushSecret controller", func() { }, }, Selector: v1alpha1.PushSecretSelector{ - Secret: v1alpha1.PushSecretSecret{ + Secret: &v1alpha1.PushSecretSecret{ Name: SecretName, }, }, @@ -515,7 +531,7 @@ var _ = Describe("PushSecret controller", func() { }, }, Selector: v1alpha1.PushSecretSelector{ - Secret: v1alpha1.PushSecretSecret{ + Secret: &v1alpha1.PushSecretSecret{ Name: SecretName, }, }, @@ -570,7 +586,7 @@ var _ = Describe("PushSecret controller", func() { }, }, Selector: v1alpha1.PushSecretSelector{ - Secret: v1alpha1.PushSecretSecret{ + Secret: &v1alpha1.PushSecretSecret{ Name: SecretName, }, }, @@ -716,7 +732,7 @@ var _ = Describe("PushSecret controller", func() { }, }, Selector: v1alpha1.PushSecretSelector{ - Secret: v1alpha1.PushSecretSecret{ + Secret: &v1alpha1.PushSecretSecret{ Name: SecretName, }, }, @@ -782,7 +798,7 @@ var _ = Describe("PushSecret controller", func() { }, }, Selector: v1alpha1.PushSecretSelector{ - Secret: v1alpha1.PushSecretSecret{ + Secret: &v1alpha1.PushSecretSecret{ Name: SecretName, }, }, @@ -861,6 +877,28 @@ var _ = Describe("PushSecret controller", func() { return bytes.Equal(secretValue, providerValue) && checkCondition(ps.Status, expected) } } + + syncWithGenerator := func(tc *testCase) { + fakeProvider.SetSecretFn = func() error { + return nil + } + tc.pushsecret.Spec.Selector.Secret = nil + tc.pushsecret.Spec.Selector.GeneratorRef = &v1beta1.GeneratorRef{ + APIVersion: "generators.external-secrets.io/v1alpha1", + Kind: "Fake", + Name: "test", + } + tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool { + providerValue := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value + expected := v1alpha1.PushSecretStatusCondition{ + Type: v1alpha1.PushSecretReady, + Status: v1.ConditionTrue, + Reason: v1alpha1.ReasonSynced, + Message: "PushSecret synced successfully", + } + return bytes.Equal([]byte("foo-bar-from-generator"), providerValue) && checkCondition(ps.Status, expected) + } + } // if target Secret name is not specified it should use the ExternalSecret name. syncWithClusterStoreMatchingLabels := func(tc *testCase) { fakeProvider.SetSecretFn = func() error { @@ -884,7 +922,7 @@ var _ = Describe("PushSecret controller", func() { }, }, Selector: v1alpha1.PushSecretSelector{ - Secret: v1alpha1.PushSecretSecret{ + Secret: &v1alpha1.PushSecretSecret{ Name: SecretName, }, }, @@ -1069,6 +1107,7 @@ var _ = Describe("PushSecret controller", func() { Entry("should sync to stores matching labels", syncMatchingLabels), Entry("should sync with ClusterStore", syncWithClusterStore), Entry("should sync with ClusterStore matching labels", syncWithClusterStoreMatchingLabels), + Entry("should sync with Generator", syncWithGenerator), Entry("should fail if Secret is not created", failNoSecret), Entry("should fail if Secret Key does not exist", failNoSecretKey), Entry("should fail if SetSecret fails", setSecretFail), @@ -1168,7 +1207,7 @@ var _ = Describe("PushSecret Controller Un/Managed Stores", func() { }, }, Selector: v1alpha1.PushSecretSelector{ - Secret: v1alpha1.PushSecretSecret{ + Secret: &v1alpha1.PushSecretSecret{ Name: SecretName, }, }, diff --git a/pkg/controllers/pushsecret/suite_test.go b/pkg/controllers/pushsecret/suite_test.go index 48ce7b03e..2d49551a7 100644 --- a/pkg/controllers/pushsecret/suite_test.go +++ b/pkg/controllers/pushsecret/suite_test.go @@ -32,6 +32,7 @@ import ( esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" + genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -72,6 +73,8 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) err = esv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = genv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme.Scheme, @@ -90,7 +93,8 @@ var _ = BeforeSuite(func() { err = (&Reconciler{ Client: k8sClient, Scheme: k8sManager.GetScheme(), - Log: ctrl.Log.WithName("controllers").WithName("ExternalSecrets"), + Log: ctrl.Log.WithName("controllers").WithName("PushSecret"), + RestConfig: cfg, RequeueInterval: time.Second, }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) diff --git a/pkg/utils/resolvers/generator.go b/pkg/utils/resolvers/generator.go new file mode 100644 index 000000000..b83d8bcd9 --- /dev/null +++ b/pkg/utils/resolvers/generator.go @@ -0,0 +1,84 @@ +/* +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 + + http://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 resolvers + +import ( + "context" + "fmt" + + apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + "k8s.io/client-go/restmapper" + + esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" + genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1" +) + +// GeneratorRef resolves a generator reference to a generator implementation. +func GeneratorRef(ctx context.Context, restConfig *rest.Config, namespace string, generatorRef *esv1beta1.GeneratorRef) (genv1alpha1.Generator, *apiextensions.JSON, error) { + obj, err := getGeneratorDefinition(ctx, restConfig, namespace, generatorRef) + if err != nil { + return nil, nil, fmt.Errorf("unable to get generator definition: %w", err) + } + generator, err := genv1alpha1.GetGenerator(obj) + if err != nil { + return nil, nil, fmt.Errorf("unable to get generator: %w", err) + } + return generator, obj, nil +} + +func getGeneratorDefinition(ctx context.Context, restConfig *rest.Config, namespace string, generatorRef *esv1beta1.GeneratorRef) (*apiextensions.JSON, error) { + // client-go dynamic client needs a GVR to fetch the resource + // But we only have the GVK in our generatorRef. + // + // TODO: there is no need to discover the GroupVersionResource + // this should be cached. + c := discovery.NewDiscoveryClientForConfigOrDie(restConfig) + groupResources, err := restmapper.GetAPIGroupResources(c) + if err != nil { + return nil, err + } + + gv, err := schema.ParseGroupVersion(generatorRef.APIVersion) + if err != nil { + return nil, err + } + mapper := restmapper.NewDiscoveryRESTMapper(groupResources) + mapping, err := mapper.RESTMapping(schema.GroupKind{ + Group: gv.Group, + Kind: generatorRef.Kind, + }) + if err != nil { + return nil, err + } + d, err := dynamic.NewForConfig(restConfig) + if err != nil { + return nil, err + } + res, err := d.Resource(mapping.Resource). + Namespace(namespace). + Get(ctx, generatorRef.Name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + jsonRes, err := res.MarshalJSON() + if err != nil { + return nil, err + } + return &apiextensions.JSON{Raw: jsonRes}, nil +}