From cfe36270f50311e5c0f99f134088704607dfd09d Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Fri, 22 Nov 2024 12:03:46 +0100 Subject: [PATCH] POC/Provisioning: create folder if it does not exist (#96890) * feat: create folder if it does not exist * feat: test the folder creation behaviour * test: make sure deletion doesn't fail * test: remove unnecessary settings --- pkg/registry/apis/provisioning/register.go | 73 ++++++++++++++++++- .../apis/provisioning/provisioning_test.go | 28 +++++++ .../provisioning/testdata/github-example.yaml | 1 + .../provisioning/testdata/local-devenv.yaml | 1 + 4 files changed, 100 insertions(+), 3 deletions(-) diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index 89da28866a2..5fdc934080f 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -11,7 +11,7 @@ import ( "slices" "github.com/prometheus/client_golang/prometheus" - "k8s.io/apimachinery/pkg/api/errors" + 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" @@ -44,6 +44,7 @@ type ProvisioningAPIBuilder struct { urlProvider func(namespace string) string webhookSecretKey string + features featuremgmt.FeatureToggles getter rest.Getter localFileResolver *LocalFolderResolver logger *slog.Logger @@ -58,6 +59,7 @@ func NewProvisioningAPIBuilder( urlProvider func(namespace string) string, webhookSecreteKey string, identities auth.BackgroundIdentityService, + features featuremgmt.FeatureToggles, ) *ProvisioningAPIBuilder { return &ProvisioningAPIBuilder{ urlProvider: urlProvider, @@ -65,6 +67,7 @@ func NewProvisioningAPIBuilder( logger: slog.Default().With("logger", "provisioning-api-builder"), webhookSecretKey: webhookSecreteKey, client: newResourceClient(identities), + features: features, } } @@ -84,7 +87,7 @@ func RegisterAPIService( DevenvPath: filepath.Join(cfg.HomePath, "devenv"), }, func(namespace string) string { return cfg.AppURL - }, cfg.SecretKey, identities) + }, cfg.SecretKey, identities, features) apiregistration.RegisterAPI(builder) return builder } @@ -208,6 +211,11 @@ func (b *ProvisioningAPIBuilder) afterCreate(obj runtime.Object, opts *metav1.Cr return } + if err := b.ensureRepositoryFolderExists(ctx, cfg); err != nil { + b.logger.Error("failed to ensure repository folder exists", "error", err) + return + } + if err := repo.AfterCreate(ctx); err != nil { b.logger.Error("failed to run after create", "error", err) return @@ -234,6 +242,10 @@ func (b *ProvisioningAPIBuilder) beginUpdate(ctx context.Context, obj, old runti return nil, fmt.Errorf("get old repository: %w", err) } + if err := b.ensureRepositoryFolderExists(ctx, objCfg); err != nil { + return nil, fmt.Errorf("failed to ensure the configured folder exists: %w", err) + } + undo, err := repo.BeginUpdate(ctx, oldRepo) if err != nil { return nil, err @@ -248,6 +260,61 @@ func (b *ProvisioningAPIBuilder) beginUpdate(ctx context.Context, obj, old runti }, nil } +func (b *ProvisioningAPIBuilder) ensureRepositoryFolderExists(ctx context.Context, cfg *provisioning.Repository) error { + if !b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) && !b.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs) { + // Nothing to do: we can't use the folders API. + return nil + } + + if cfg.Spec.Folder == "" { + // The root folder can't not exist, so we don't have to do anything. + return nil + } + + client, err := b.client.Client(cfg.GetNamespace()) + if err != nil { + return err + } + + lookup := newKindsLookup(client) + folderResource, ok := lookup.Resource(schema.GroupVersionKind{ + Group: "folder.grafana.app", + Version: "v0alpha1", + Kind: "Folder", + }) + if !ok { + return fmt.Errorf("failed to get resource client of the Folder kind") + } + folderIface := client.Resource(folderResource).Namespace(cfg.GetNamespace()) + + _, err = folderIface.Get(ctx, cfg.Spec.Folder, metav1.GetOptions{}) + if err == nil { + // The folder exists and doesn't need to be created. + return nil + } else if !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to search for existing repo folder: %w", err) + } + + title := cfg.Spec.Title + if title == "" { + title = cfg.Spec.Folder + } + + _, err = folderIface.Create(ctx, &unstructured.Unstructured{ + Object: map[string]any{ + "metadata": map[string]any{ + "name": cfg.Spec.Folder, + "namespace": cfg.GetNamespace(), + }, + "spec": map[string]any{ + "title": title, + "description": "Repository-managed folder.", + }, + }, + }, metav1.CreateOptions{}) + return err +} + func (b *ProvisioningAPIBuilder) afterDelete(obj runtime.Object, opts *metav1.DeleteOptions) { ctx := context.Background() cfg, ok := obj.(*provisioning.Repository) @@ -357,7 +424,7 @@ func (b *ProvisioningAPIBuilder) Validate(ctx context.Context, a admission.Attri } if len(list) > 0 { - return errors.NewInvalid(schema.GroupKind{ + return apierrors.NewInvalid(schema.GroupKind{ Group: provisioning.GROUP, Kind: "Repository", //?? }, a.GetName(), list) diff --git a/pkg/tests/apis/provisioning/provisioning_test.go b/pkg/tests/apis/provisioning/provisioning_test.go index 84920c95850..18b47983a51 100644 --- a/pkg/tests/apis/provisioning/provisioning_test.go +++ b/pkg/tests/apis/provisioning/provisioning_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "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" @@ -31,6 +32,7 @@ func TestIntegrationProvisioning(t *testing.T) { AppModeProduction: false, // required for experimental APIs EnableFeatureToggles: []string{ featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service + featuremgmt.FlagKubernetesFolders, // Required for tests that deal with folders. }, }) @@ -43,6 +45,14 @@ func TestIntegrationProvisioning(t *testing.T) { }, }) + folderClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: "default", // actually org1 + GVR: schema.GroupVersionResource{ + Group: "folder.grafana.app", Version: "v0alpha1", Resource: "folders", + }, + }) + t.Run("Check discovery client", func(t *testing.T) { disco := helper.NewDiscoveryClient() resources, err := disco.ServerResourcesForGroupVersion("provisioning.grafana.app/v0alpha1") @@ -258,6 +268,24 @@ func TestIntegrationProvisioning(t *testing.T) { "World", mustNestedString(resp.Object, "whom")) }) + + t.Run("creating repository creates folder", func(t *testing.T) { + // Just make sure the folder doesn't exist in advance. + err := folderClient.Resource.Delete(ctx, "thisisafolderref", metav1.DeleteOptions{}) + if err != nil && !errors.IsNotFound(err) { + require.NoError(t, err, "deletion should either be OK or fail with NotFound") + } + + _, err = client.Resource.Update(ctx, + helper.LoadYAMLOrJSONFile("testdata/github-example.yaml"), + metav1.UpdateOptions{}, + ) + require.NoError(t, err) + + resp, err := folderClient.Resource.Get(ctx, "thisisafolderref", metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, "thisisafolderref", mustNestedString(resp.Object, "metadata", "name")) + }) } func mustNestedString(obj map[string]interface{}, fields ...string) string { diff --git a/pkg/tests/apis/provisioning/testdata/github-example.yaml b/pkg/tests/apis/provisioning/testdata/github-example.yaml index fb3d690fc47..df5abd3c1de 100644 --- a/pkg/tests/apis/provisioning/testdata/github-example.yaml +++ b/pkg/tests/apis/provisioning/testdata/github-example.yaml @@ -6,6 +6,7 @@ spec: title: Github Example description: load resources from github type: github + folder: thisisafolderref editing: create: true update: true diff --git a/pkg/tests/apis/provisioning/testdata/local-devenv.yaml b/pkg/tests/apis/provisioning/testdata/local-devenv.yaml index 55d3b76cc01..cd674f0cf62 100644 --- a/pkg/tests/apis/provisioning/testdata/local-devenv.yaml +++ b/pkg/tests/apis/provisioning/testdata/local-devenv.yaml @@ -5,6 +5,7 @@ metadata: spec: title: Load devenv dashboards description: load https://github.com/grafana/grafana/tree/main/devenv/dev-dashboards + folder: testingtesting editing: create: true update: true