|
@@ -17,146 +17,198 @@ package resolvers
|
|
|
import (
|
|
import (
|
|
|
"context"
|
|
"context"
|
|
|
"fmt"
|
|
"fmt"
|
|
|
|
|
+ "reflect"
|
|
|
|
|
|
|
|
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
|
- "k8s.io/apimachinery/pkg/api/meta"
|
|
|
|
|
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
|
|
|
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
|
|
|
+ "k8s.io/apimachinery/pkg/runtime"
|
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
- "k8s.io/apimachinery/pkg/util/json"
|
|
|
|
|
- "k8s.io/client-go/discovery"
|
|
|
|
|
- "k8s.io/client-go/dynamic"
|
|
|
|
|
- "k8s.io/client-go/rest"
|
|
|
|
|
- "k8s.io/client-go/restmapper"
|
|
|
|
|
|
|
+ "k8s.io/apimachinery/pkg/types"
|
|
|
|
|
+ "sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
|
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
|
|
|
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
|
|
genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
|
|
genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+// these errors are explicitly defined so we can detect them with `errors.Is()`.
|
|
|
|
|
+var (
|
|
|
|
|
+ // ErrUnableToGetGenerator is returned when a generator reference cannot be resolved.
|
|
|
|
|
+ ErrUnableToGetGenerator = fmt.Errorf("unable to get generator")
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
// GeneratorRef resolves a generator reference to a generator implementation.
|
|
// 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)
|
|
|
|
|
|
|
+func GeneratorRef(ctx context.Context, cl client.Client, scheme *runtime.Scheme, namespace string, generatorRef *esv1beta1.GeneratorRef) (genv1alpha1.Generator, *apiextensions.JSON, error) {
|
|
|
|
|
+ generator, jsonObj, err := getGenerator(ctx, cl, scheme, namespace, generatorRef)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- return nil, nil, fmt.Errorf("unable to get generator: %w", err)
|
|
|
|
|
|
|
+ return nil, nil, fmt.Errorf("%w: %w", ErrUnableToGetGenerator, err)
|
|
|
}
|
|
}
|
|
|
- return generator, obj, nil
|
|
|
|
|
|
|
+ return generator, jsonObj, 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
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+func getGenerator(ctx context.Context, cl client.Client, scheme *runtime.Scheme, namespace string, generatorRef *esv1beta1.GeneratorRef) (genv1alpha1.Generator, *apiextensions.JSON, error) {
|
|
|
|
|
+ // get a GVK from the generatorRef
|
|
|
gv, err := schema.ParseGroupVersion(generatorRef.APIVersion)
|
|
gv, err := schema.ParseGroupVersion(generatorRef.APIVersion)
|
|
|
if err != nil {
|
|
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
|
|
|
|
|
|
|
+ return nil, nil, reconcile.TerminalError(fmt.Errorf("generatorRef has invalid APIVersion: %w", err))
|
|
|
}
|
|
}
|
|
|
- d, err := dynamic.NewForConfig(restConfig)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, err
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if generatorRef.Kind == "ClusterGenerator" {
|
|
|
|
|
- return extractGeneratorFromClusterGenerator(ctx, d, mapping, generatorRef)
|
|
|
|
|
|
|
+ gvk := schema.GroupVersionKind{
|
|
|
|
|
+ Group: gv.Group,
|
|
|
|
|
+ Version: gv.Version,
|
|
|
|
|
+ Kind: generatorRef.Kind,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- res, err := d.Resource(mapping.Resource).Namespace(namespace).Get(ctx, generatorRef.Name, metav1.GetOptions{})
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, err
|
|
|
|
|
|
|
+ // fail if the GVK does not use the generator group
|
|
|
|
|
+ if gvk.Group != genv1alpha1.Group {
|
|
|
|
|
+ return nil, nil, reconcile.TerminalError(fmt.Errorf("generatorRef may only reference the generators group, but got %s", gvk.Group))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- jsonRes, err := res.MarshalJSON()
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, err
|
|
|
|
|
|
|
+ // get a client Object from the GVK
|
|
|
|
|
+ t, exists := scheme.AllKnownTypes()[gvk]
|
|
|
|
|
+ if !exists {
|
|
|
|
|
+ return nil, nil, reconcile.TerminalError(fmt.Errorf("generatorRef references unknown GVK %s", gvk))
|
|
|
}
|
|
}
|
|
|
- return &apiextensions.JSON{Raw: jsonRes}, nil
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ obj := reflect.New(t).Interface().(client.Object)
|
|
|
|
|
|
|
|
-func extractGeneratorFromClusterGenerator(
|
|
|
|
|
- ctx context.Context,
|
|
|
|
|
- d *dynamic.DynamicClient,
|
|
|
|
|
- mapping *meta.RESTMapping,
|
|
|
|
|
- generatorRef *esv1beta1.GeneratorRef,
|
|
|
|
|
-) (*apiextensions.JSON, error) {
|
|
|
|
|
- res, err := d.Resource(mapping.Resource).Get(ctx, generatorRef.Name, metav1.GetOptions{})
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, err
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // this interface provides the Generate() method used by the controller
|
|
|
|
|
+ // NOTE: all instances of a generator kind use the same instance of this interface
|
|
|
|
|
+ var generator genv1alpha1.Generator
|
|
|
|
|
|
|
|
- spec, err := extractValue[map[string]any](res.Object, genv1alpha1.GeneratorSpecKey)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, err
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // ClusterGenerator is a special case because it's a cluster-scoped resource
|
|
|
|
|
+ // to use it, we create a "virtual" namespaced generator for the current namespace, as if one existed in the API
|
|
|
|
|
+ if gvk.Kind == genv1alpha1.ClusterGeneratorKind {
|
|
|
|
|
+ clusterGenerator := obj.(*genv1alpha1.ClusterGenerator)
|
|
|
|
|
|
|
|
- generator, err := extractValue[map[string]any](spec, genv1alpha1.GeneratorGeneratorKey)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, err
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // get the cluster generator resource from the API
|
|
|
|
|
+ // NOTE: it's important that we use the structured client so we use the cache
|
|
|
|
|
+ err = cl.Get(ctx, client.ObjectKey{Name: generatorRef.Name}, clusterGenerator)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, nil, err
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- kind, err := extractValue[string](spec, genv1alpha1.GeneratorKindKey)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- return nil, err
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // convert the cluster generator to a virtual namespaced generator object
|
|
|
|
|
+ obj, err = clusterGeneratorToVirtual(clusterGenerator)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, nil, reconcile.TerminalError(fmt.Errorf("invalid ClusterGenerator: %w", err))
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // find the first value and that's what we are going to take
|
|
|
|
|
- // this will be the generator that has been set by the user
|
|
|
|
|
- var result []byte
|
|
|
|
|
- for _, v := range generator {
|
|
|
|
|
- vMap, ok := v.(map[string]interface{})
|
|
|
|
|
|
|
+ // get the generator interface
|
|
|
|
|
+ var ok bool
|
|
|
|
|
+ generator, ok = genv1alpha1.GetGeneratorByName(clusterGenerator.Spec.Kind)
|
|
|
if !ok {
|
|
if !ok {
|
|
|
- return nil, fmt.Errorf("kind was not of object type for cluster generator %T", v)
|
|
|
|
|
|
|
+ return nil, nil, reconcile.TerminalError(fmt.Errorf("ClusterGenerator has unknown kind %s", clusterGenerator.Spec.Kind))
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // Construct our generator object so it can be later unmarshalled into a valid Generator Spec.
|
|
|
|
|
- object := map[string]interface{}{}
|
|
|
|
|
- object["kind"] = kind
|
|
|
|
|
- object["spec"] = vMap
|
|
|
|
|
- result, err = json.Marshal(object)
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // get the generator resource from the API
|
|
|
|
|
+ // NOTE: it's important that we use the structured client so we use the cache
|
|
|
|
|
+ err = cl.Get(ctx, types.NamespacedName{
|
|
|
|
|
+ Name: generatorRef.Name,
|
|
|
|
|
+ Namespace: namespace,
|
|
|
|
|
+ }, obj)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
- return nil, err
|
|
|
|
|
|
|
+ return nil, nil, err
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return &apiextensions.JSON{Raw: result}, nil
|
|
|
|
|
|
|
+ // get the generator interface
|
|
|
|
|
+ var ok bool
|
|
|
|
|
+ generator, ok = genv1alpha1.GetGeneratorByName(gvk.Kind)
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ return nil, nil, reconcile.TerminalError(fmt.Errorf("generatorRef has unknown kind %s", gvk.Kind))
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return nil, fmt.Errorf("no defined generators found for cluster generator spec: %v", spec)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// extractValue fetches a specific key value that we are looking for in a map.
|
|
|
|
|
-func extractValue[T any](m any, k string) (T, error) {
|
|
|
|
|
- var result T
|
|
|
|
|
- v, ok := m.(map[string]any)
|
|
|
|
|
- if !ok {
|
|
|
|
|
- return result, fmt.Errorf("value was not of type map[string]any but: %T", m)
|
|
|
|
|
|
|
+ // convert the generator to unstructured object
|
|
|
|
|
+ u := &unstructured.Unstructured{}
|
|
|
|
|
+ u.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, nil, err
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- vv, ok := v[k]
|
|
|
|
|
- if !ok {
|
|
|
|
|
- return result, fmt.Errorf("key %s was not found in map", k)
|
|
|
|
|
|
|
+ // convert the unstructured object to JSON
|
|
|
|
|
+ // NOTE: we do this for backwards compatibility with how this API works, not because it's a good idea
|
|
|
|
|
+ // we should refactor the generator API to use the normal typed objects
|
|
|
|
|
+ jsonObj, err := u.MarshalJSON()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, nil, err
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- vvv, ok := vv.(T)
|
|
|
|
|
- if !ok {
|
|
|
|
|
- return result, fmt.Errorf("value was not of type T but: %T", vvv)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return generator, &apiextensions.JSON{Raw: jsonObj}, nil
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- return vvv, nil
|
|
|
|
|
|
|
+// clusterGeneratorToVirtual converts a ClusterGenerator to a "virtual" namespaced generator that doesn't actually exist in the API.
|
|
|
|
|
+func clusterGeneratorToVirtual(gen *genv1alpha1.ClusterGenerator) (client.Object, error) {
|
|
|
|
|
+ switch gen.Spec.Kind {
|
|
|
|
|
+ case genv1alpha1.ACRAccessTokenKind:
|
|
|
|
|
+ if gen.Spec.Generator.ACRAccessTokenSpec == nil {
|
|
|
|
|
+ return nil, fmt.Errorf("when kind is %s, ACRAccessTokenSpec must be set", gen.Spec.Kind)
|
|
|
|
|
+ }
|
|
|
|
|
+ return &genv1alpha1.ACRAccessToken{
|
|
|
|
|
+ Spec: *gen.Spec.Generator.ACRAccessTokenSpec,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ case genv1alpha1.ECRAuthorizationTokenKind:
|
|
|
|
|
+ if gen.Spec.Generator.ECRAuthorizationTokenSpec == nil {
|
|
|
|
|
+ return nil, fmt.Errorf("when kind is %s, ECRAuthorizationTokenSpec must be set", gen.Spec.Kind)
|
|
|
|
|
+ }
|
|
|
|
|
+ return &genv1alpha1.ECRAuthorizationToken{
|
|
|
|
|
+ Spec: *gen.Spec.Generator.ECRAuthorizationTokenSpec,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ case genv1alpha1.FakeKind:
|
|
|
|
|
+ if gen.Spec.Generator.FakeSpec == nil {
|
|
|
|
|
+ return nil, fmt.Errorf("when kind is %s, FakeSpec must be set", gen.Spec.Kind)
|
|
|
|
|
+ }
|
|
|
|
|
+ return &genv1alpha1.Fake{
|
|
|
|
|
+ Spec: *gen.Spec.Generator.FakeSpec,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ case genv1alpha1.GCRAccessTokenKind:
|
|
|
|
|
+ if gen.Spec.Generator.GCRAccessTokenSpec == nil {
|
|
|
|
|
+ return nil, fmt.Errorf("when kind is %s, GCRAccessTokenSpec must be set", gen.Spec.Kind)
|
|
|
|
|
+ }
|
|
|
|
|
+ return &genv1alpha1.GCRAccessToken{
|
|
|
|
|
+ Spec: *gen.Spec.Generator.GCRAccessTokenSpec,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ case genv1alpha1.GithubAccessTokenKind:
|
|
|
|
|
+ if gen.Spec.Generator.GithubAccessTokenSpec == nil {
|
|
|
|
|
+ return nil, fmt.Errorf("when kind is %s, GithubAccessTokenSpec must be set", gen.Spec.Kind)
|
|
|
|
|
+ }
|
|
|
|
|
+ return &genv1alpha1.GithubAccessToken{
|
|
|
|
|
+ Spec: *gen.Spec.Generator.GithubAccessTokenSpec,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ case genv1alpha1.PasswordKind:
|
|
|
|
|
+ if gen.Spec.Generator.PasswordSpec == nil {
|
|
|
|
|
+ return nil, fmt.Errorf("when kind is %s, PasswordSpec must be set", gen.Spec.Kind)
|
|
|
|
|
+ }
|
|
|
|
|
+ return &genv1alpha1.Password{
|
|
|
|
|
+ Spec: *gen.Spec.Generator.PasswordSpec,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ case genv1alpha1.STSSessionTokenKind:
|
|
|
|
|
+ if gen.Spec.Generator.STSSessionTokenSpec == nil {
|
|
|
|
|
+ return nil, fmt.Errorf("when kind is %s, STSSessionTokenSpec must be set", gen.Spec.Kind)
|
|
|
|
|
+ }
|
|
|
|
|
+ return &genv1alpha1.STSSessionToken{
|
|
|
|
|
+ Spec: *gen.Spec.Generator.STSSessionTokenSpec,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ case genv1alpha1.UUIDKind:
|
|
|
|
|
+ if gen.Spec.Generator.UUIDSpec == nil {
|
|
|
|
|
+ return nil, fmt.Errorf("when kind is %s, UUIDSpec must be set", gen.Spec.Kind)
|
|
|
|
|
+ }
|
|
|
|
|
+ return &genv1alpha1.UUID{
|
|
|
|
|
+ Spec: *gen.Spec.Generator.UUIDSpec,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ case genv1alpha1.VaultDynamicSecretKind:
|
|
|
|
|
+ if gen.Spec.Generator.VaultDynamicSecretSpec == nil {
|
|
|
|
|
+ return nil, fmt.Errorf("when kind is %s, VaultDynamicSecretSpec must be set", gen.Spec.Kind)
|
|
|
|
|
+ }
|
|
|
|
|
+ return &genv1alpha1.VaultDynamicSecret{
|
|
|
|
|
+ Spec: *gen.Spec.Generator.VaultDynamicSecretSpec,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ case genv1alpha1.WebhookKind:
|
|
|
|
|
+ if gen.Spec.Generator.WebhookSpec == nil {
|
|
|
|
|
+ return nil, fmt.Errorf("when kind is %s, WebhookSpec must be set", gen.Spec.Kind)
|
|
|
|
|
+ }
|
|
|
|
|
+ return &genv1alpha1.Webhook{
|
|
|
|
|
+ Spec: *gen.Spec.Generator.WebhookSpec,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+ default:
|
|
|
|
|
+ return nil, fmt.Errorf("unknown kind %s", gen.Spec.Kind)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|