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
This commit is contained in:
Mariell Hoversholm authored and GitHub committed 2024-11-22 12:03:46 +01:00
1 parent f8fedd2698
commit cfe36270f5
4 files changed
+100 -3

No files matched your search

+70 -3
View File
@@ -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)
@@ -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 {
@@ -6,6 +6,7 @@ spec:
title: Github Example
description: load resources from github
type: github
folder: thisisafolderref
editing:
create: true
update: true
@@ -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