1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

fix: use cache when retrieving generators (#4153)

* fix: use cache when retrieving generators

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>

* fix longstanding schema issues

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>

---------

Signed-off-by: Mathew Wicks <5735406+thesuperzapper@users.noreply.github.com>
Co-authored-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
This commit is contained in:
Mathew Wicks 2024-11-28 00:28:21 -08:00 committed by GitHub
parent e65bf8d402
commit 73bff05bf2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 225 additions and 167 deletions

View file

@ -17,10 +17,6 @@ package v1alpha1
import (
"fmt"
"sync"
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/json"
)
var builder map[string]Generator
@ -58,24 +54,3 @@ func GetGeneratorByName(kind string) (Generator, bool) {
buildlock.RUnlock()
return f, ok
}
// GetGenerator returns an implementation from a generator
// defined as json.
func GetGenerator(obj *apiextensions.JSON) (Generator, error) {
type unknownGenerator struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
}
var res unknownGenerator
err := json.Unmarshal(obj.Raw, &res)
if err != nil {
return nil, err
}
buildlock.RLock()
defer buildlock.RUnlock()
gen, ok := builder[res.Kind]
if !ok {
return nil, fmt.Errorf("failed to find registered generator for: %s with kind: %s", string(obj.Raw), res.Kind)
}
return gen, nil
}

View file

@ -32,6 +32,10 @@ type ControllerClassResource struct {
}
type GeneratorSpec struct {
// NOTE: when adding new supported generators, make sure to also update
// clusterGeneratorToVirtual() function in pkg/utils/resolvers/generator.go
// so they can be unpacked correctly.
ACRAccessTokenSpec *ACRAccessTokenSpec `json:"acrAccessTokenSpec,omitempty"`
ECRAuthorizationTokenSpec *ECRAuthorizationTokenSpec `json:"ecrRAuthorizationTokenSpec,omitempty"`
FakeSpec *FakeSpec `json:"fakeSpec,omitempty"`

View file

@ -125,7 +125,7 @@ var (
)
func init() {
SchemeBuilder.Register(&ECRAuthorizationToken{}, &ECRAuthorizationToken{})
SchemeBuilder.Register(&ECRAuthorizationToken{}, &ECRAuthorizationTokenList{})
SchemeBuilder.Register(&GCRAccessToken{}, &GCRAccessTokenList{})
SchemeBuilder.Register(&GithubAccessToken{}, &GithubAccessTokenList{})
SchemeBuilder.Register(&ACRAccessToken{}, &ACRAccessTokenList{})

View file

@ -25,6 +25,7 @@ import (
v1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
@ -94,11 +95,14 @@ const (
)
func init() {
_ = clientgoscheme.AddToScheme(scheme)
_ = esv1beta1.AddToScheme(scheme)
_ = esv1alpha1.AddToScheme(scheme)
_ = genv1alpha1.AddToScheme(scheme)
_ = apiextensionsv1.AddToScheme(scheme)
// kubernetes schemes
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
// external-secrets schemes
utilruntime.Must(esv1beta1.AddToScheme(scheme))
utilruntime.Must(esv1alpha1.AddToScheme(scheme))
utilruntime.Must(genv1alpha1.AddToScheme(scheme))
}
var rootCmd = &cobra.Command{

View file

@ -29,6 +29,7 @@ import (
"github.com/spf13/cobra"
"go.uber.org/zap/zapcore"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
@ -45,9 +46,12 @@ const (
)
func init() {
_ = clientgoscheme.AddToScheme(scheme)
_ = esv1beta1.AddToScheme(scheme)
_ = esv1alpha1.AddToScheme(scheme)
// kubernetes schemes
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
// external-secrets schemes
utilruntime.Must(esv1beta1.AddToScheme(scheme))
utilruntime.Must(esv1alpha1.AddToScheme(scheme))
}
var webhookCmd = &cobra.Command{

View file

@ -24,37 +24,43 @@ import (
fluxhelm "github.com/fluxcd/helm-controller/api/v2beta1"
fluxsrc "github.com/fluxcd/source-controller/api/v1beta2"
// nolint
. "github.com/onsi/ginkgo/v2"
v1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/remotecommand"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
// nolint
. "github.com/onsi/ginkgo/v2"
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"
)
var Scheme = runtime.NewScheme()
var scheme = runtime.NewScheme()
func init() {
_ = scheme.AddToScheme(Scheme)
_ = esv1beta1.AddToScheme(Scheme)
_ = esv1alpha1.AddToScheme(Scheme)
_ = genv1alpha1.AddToScheme(Scheme)
_ = fluxhelm.AddToScheme(Scheme)
_ = fluxsrc.AddToScheme(Scheme)
_ = apiextensionsv1.AddToScheme(Scheme)
// kubernetes schemes
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
// external-secrets schemes
utilruntime.Must(esv1beta1.AddToScheme(scheme))
utilruntime.Must(esv1alpha1.AddToScheme(scheme))
utilruntime.Must(genv1alpha1.AddToScheme(scheme))
// other schemes
utilruntime.Must(fluxhelm.AddToScheme(scheme))
utilruntime.Must(fluxsrc.AddToScheme(scheme))
}
const (
@ -129,7 +135,7 @@ func execCmd(client kubernetes.Interface, config *restclient.Config, podName, co
}
req.VersionedParams(
option,
scheme.ParameterCodec,
clientgoscheme.ParameterCodec,
)
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
if err != nil {
@ -290,7 +296,7 @@ func NewConfig() (*restclient.Config, *kubernetes.Clientset, crclient.Client) {
Fail(err.Error())
}
CRClient, err := crclient.New(kubeConfig, crclient.Options{Scheme: Scheme})
CRClient, err := crclient.New(kubeConfig, crclient.Options{Scheme: scheme})
if err != nil {
Fail(err.Error())
}

View file

@ -799,12 +799,16 @@ func shouldSkipUnmanagedStore(ctx context.Context, namespace string, r *Reconcil
// verify that generator's controllerClass matches
if ref.SourceRef != nil && ref.SourceRef.GeneratorRef != nil {
_, obj, err := resolvers.GeneratorRef(ctx, r.RestConfig, namespace, ref.SourceRef.GeneratorRef)
_, obj, err := resolvers.GeneratorRef(ctx, r.Client, r.Scheme, namespace, ref.SourceRef.GeneratorRef)
if err != nil {
if apierrors.IsNotFound(err) {
// skip non-existent generators
continue
}
if errors.Is(err, resolvers.ErrUnableToGetGenerator) {
// skip generators that we can't get (e.g. due to being invalid)
continue
}
return false, err
}
skipGenerator, err := shouldSkipGenerator(r, obj)

View file

@ -111,7 +111,7 @@ 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) {
gen, obj, err := resolvers.GeneratorRef(ctx, r.RestConfig, namespace, remoteRef.SourceRef.GeneratorRef)
gen, obj, err := resolvers.GeneratorRef(ctx, r.Client, r.Scheme, namespace, remoteRef.SourceRef.GeneratorRef)
if err != nil {
return nil, fmt.Errorf("unable to resolve generator: %w", err)
}

View file

@ -372,7 +372,7 @@ func (r *Reconciler) resolveSecret(ctx context.Context, ps esapi.PushSecret) (*v
}
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)
gen, obj, err := resolvers.GeneratorRef(ctx, r.Client, r.Scheme, namespace, generatorRef)
if err != nil {
return nil, fmt.Errorf("unable to resolve generator: %w", err)
}

View file

@ -25,6 +25,7 @@ import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
@ -35,9 +36,13 @@ import (
func TestManagerGet(t *testing.T) {
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = esv1beta1.AddToScheme(scheme)
_ = apiextensionsv1.AddToScheme(scheme)
// add kubernetes schemes
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
// add external-secrets schemes
utilruntime.Must(esv1beta1.AddToScheme(scheme))
// We have a test provider to control
// the behavior of the NewClient func.
@ -312,9 +317,13 @@ func TestManagerGet(t *testing.T) {
func TestShouldProcessSecret(t *testing.T) {
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = esv1beta1.AddToScheme(scheme)
_ = apiextensionsv1.AddToScheme(scheme)
// add kubernetes schemes
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
// add external-secrets schemes
utilruntime.Must(esv1beta1.AddToScheme(scheme))
testNamespace := "test-a"
testCases := []struct {

View file

@ -17,146 +17,198 @@ package resolvers
import (
"context"
"fmt"
"reflect"
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/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"
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.
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)
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 {
return nil, nil, fmt.Errorf("unable to get generator definition: %w", err)
return nil, nil, fmt.Errorf("%w: %w", ErrUnableToGetGenerator, err)
}
generator, err := genv1alpha1.GetGenerator(obj)
if err != nil {
return nil, nil, fmt.Errorf("unable to get generator: %w", 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)
if err != nil {
return nil, err
return nil, nil, reconcile.TerminalError(fmt.Errorf("generatorRef has invalid APIVersion: %w", err))
}
mapper := restmapper.NewDiscoveryRESTMapper(groupResources)
mapping, err := mapper.RESTMapping(schema.GroupKind{
gvk := schema.GroupVersionKind{
Group: gv.Group,
Version: gv.Version,
Kind: generatorRef.Kind,
})
if err != nil {
return nil, err
}
d, err := dynamic.NewForConfig(restConfig)
if err != nil {
return nil, err
}
if generatorRef.Kind == "ClusterGenerator" {
return extractGeneratorFromClusterGenerator(ctx, d, mapping, generatorRef)
// 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))
}
res, err := d.Resource(mapping.Resource).Namespace(namespace).Get(ctx, generatorRef.Name, metav1.GetOptions{})
// 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))
}
obj := reflect.New(t).Interface().(client.Object)
// 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
// 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)
// 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, err
return nil, nil, err
}
jsonRes, err := res.MarshalJSON()
// convert the cluster generator to a virtual namespaced generator object
obj, err = clusterGeneratorToVirtual(clusterGenerator)
if err != nil {
return nil, err
return nil, nil, reconcile.TerminalError(fmt.Errorf("invalid ClusterGenerator: %w", err))
}
return &apiextensions.JSON{Raw: jsonRes}, nil
// get the generator interface
var ok bool
generator, ok = genv1alpha1.GetGeneratorByName(clusterGenerator.Spec.Kind)
if !ok {
return nil, nil, reconcile.TerminalError(fmt.Errorf("ClusterGenerator has unknown kind %s", clusterGenerator.Spec.Kind))
}
} 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 {
return nil, nil, err
}
// 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))
}
}
// convert the generator to unstructured object
u := &unstructured.Unstructured{}
u.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, nil, err
}
// 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
}
return generator, &apiextensions.JSON{Raw: jsonObj}, nil
}
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
// 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)
}
spec, err := extractValue[map[string]any](res.Object, genv1alpha1.GeneratorSpecKey)
if err != nil {
return nil, err
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)
}
generator, err := extractValue[map[string]any](spec, genv1alpha1.GeneratorGeneratorKey)
if err != nil {
return nil, err
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)
}
kind, err := extractValue[string](spec, genv1alpha1.GeneratorKindKey)
if err != nil {
return nil, err
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)
}
// 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{})
if !ok {
return nil, fmt.Errorf("kind was not of object type for cluster generator %T", v)
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)
}
// 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)
if err != nil {
return nil, err
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 &apiextensions.JSON{Raw: result}, nil
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)
}
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)
}
vv, ok := v[k]
if !ok {
return result, fmt.Errorf("key %s was not found in map", k)
}
vvv, ok := vv.(T)
if !ok {
return result, fmt.Errorf("value was not of type T but: %T", vvv)
}
return vvv, nil
}