Files
grafana/pkg/registry/apis/provisioning/resources/replicator.go
T

190 lines
5.4 KiB
Go

package resources
import (
"context"
"errors"
"fmt"
"log/slog"
"path/filepath"
"strings"
apiutils "github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
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/schema"
"k8s.io/client-go/dynamic"
)
type ReplicatorFactory struct {
repo repository.Repository
client *ClientFactory
namespace string
}
func NewReplicatorFactory(client *ClientFactory, namespace string, repo repository.Repository) *ReplicatorFactory {
return &ReplicatorFactory{
client: client,
namespace: namespace,
repo: repo,
}
}
func (f *ReplicatorFactory) New() (repository.FileReplicator, error) {
dynamicClient, kinds, err := f.client.New(f.namespace)
if err != nil {
return nil, fmt.Errorf("failed to get client for namespace %s: %w", f.namespace, err)
}
parser := NewParser(f.repo, dynamicClient, kinds)
folders := dynamicClient.Resource(schema.GroupVersionResource{
Group: "folder.grafana.app",
Version: "v0alpha1",
Resource: "folders",
})
return &replicator{
logger: slog.Default().With("logger", "replicator", "namespace", f.namespace),
parser: parser,
client: dynamicClient,
folders: folders,
repository: f.repo,
}, nil
}
type replicator struct {
logger *slog.Logger
client *DynamicClient
parser *FileParser
folders dynamic.ResourceInterface
repository repository.Repository
}
// Replicate creates a new resource in the cluster.
// If the resource already exists, it will be updated.
func (r *replicator) Replicate(ctx context.Context, fileInfo *repository.FileInfo) error {
file, err := r.parseResource(ctx, fileInfo)
if err != nil {
return err
}
parent, err := r.createFolderPath(ctx, fileInfo.Path)
if err != nil {
return fmt.Errorf("failed to create folder path: %w", err)
}
_, err = file.Client.Get(ctx, file.Obj.GetName(), metav1.GetOptions{})
// FIXME: Remove the 'false &&' when .Get returns 404 on 404 instead of 500. Until then, this is a really ugly workaround.
if false && err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("failed to check if object already exists: %w", err)
}
if parent != "" {
file.Meta.SetFolder(parent)
}
if err != nil { // IsNotFound
_, err = file.Client.Create(ctx, file.Obj, metav1.CreateOptions{})
} else { // already exists
_, err = file.Client.Update(ctx, file.Obj, metav1.UpdateOptions{})
}
if err != nil {
return fmt.Errorf("failed to upsert object: %w", err)
}
r.logger.InfoContext(ctx, "Replicated file", "name", file.Obj.GetName(), "path", fileInfo.Path)
return nil
}
func (r *replicator) createFolderPath(ctx context.Context, filePath string) (string, error) {
dir := filepath.Dir(filePath)
parent := r.repository.Config().Spec.Folder
if dir == "." {
return parent, nil
}
logger := r.logger.With("file", filePath)
for _, folder := range strings.Split(dir, string(filepath.Separator)) {
logger := logger.With("folder", folder)
obj, err := r.folders.Get(ctx, folder, metav1.GetOptions{})
// FIXME: Check for IsNotFound properly
if obj != nil || err == nil {
logger.DebugContext(ctx, "folder already existed")
continue
}
_, err = r.folders.Create(ctx, &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]any{
"name": folder,
"namespace": r.client.GetNamespace(),
"annotations": map[string]any{
apiutils.AnnoKeyFolder: parent,
},
},
"spec": map[string]any{
"title": folder, // TODO: how do we want to get this?
"description": "Repository-managed folder.",
},
},
}, metav1.CreateOptions{})
if err != nil {
return parent, fmt.Errorf("failed to create folder %s: %w", folder, err)
}
parent = folder
logger.InfoContext(ctx, "folder created")
}
return parent, nil
}
func (r *replicator) Delete(ctx context.Context, fileInfo *repository.FileInfo) error {
file, err := r.parseResource(ctx, fileInfo)
if err != nil {
return err
}
_, err = file.Client.Get(ctx, file.Obj.GetName(), metav1.GetOptions{})
// FIXME: Remove the 'false &&' when .Get returns 404 on 404 instead of 500. Until then, this is a really ugly workaround.
if false && err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("failed to check if object already exists: %w", err)
}
if err != nil { // IsNotFound
return fmt.Errorf("get object to delete: %w", err)
}
if err = file.Client.Delete(ctx, file.Obj.GetName(), metav1.DeleteOptions{}); err != nil {
return fmt.Errorf("failed to delete object: %w", err)
}
// TODO: delete folders if empty recursively
r.logger.InfoContext(ctx, "Deleted file", "name", file.Obj.GetName(), "path", fileInfo.Path)
return nil
}
func (r *replicator) parseResource(ctx context.Context, fileInfo *repository.FileInfo) (*ParsedFile, error) {
// NOTE: We're validating here to make sure we want the folders to be created.
// If the file isn't valid, its folders aren't relevant, either.
file, err := r.parser.Parse(ctx, r.logger, fileInfo, true)
if err != nil {
return nil, fmt.Errorf("failed to parse file %s: %w", fileInfo.Path, err)
}
if file.GVR == nil {
return nil, errors.New("parsed file is missing GVR")
}
if file.Client == nil {
return nil, errors.New("parsed file is missing client")
}
return file, nil
}