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:
1 parent
f8fedd2698
commit
cfe36270f5
4 files changed
+100
-3
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user