190 lines
5.4 KiB
Go
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
|
|
}
|