Files

214 lines
5.9 KiB
Go

package resources
import (
"bytes"
"context"
"errors"
"fmt"
"slices"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
)
var (
ErrAlreadyInRepository = errors.New("already in repository")
ErrMissingName = field.Required(field.NewPath("name", "metadata", "name"), "missing name in resource")
)
type WriteOptions struct {
Path string
Ref string
}
type resourceID struct {
Name string
Resource string
Group string
}
type ResourcesManager struct {
repo repository.ReaderWriter
folders *FolderManager
parser Parser
clients ResourceClients
resourcesLookup map[resourceID]string // the path with this k8s name
}
func NewResourcesManager(repo repository.ReaderWriter, folders *FolderManager, parser Parser, clients ResourceClients) *ResourcesManager {
return &ResourcesManager{
repo: repo,
folders: folders,
parser: parser,
clients: clients,
resourcesLookup: map[resourceID]string{},
}
}
// CreateResource writes an object to the repository
func (r *ResourcesManager) WriteResourceFileFromObject(ctx context.Context, obj *unstructured.Unstructured, options WriteOptions) (string, error) {
if err := ctx.Err(); err != nil {
return "", fmt.Errorf("context error: %w", err)
}
meta, err := utils.MetaAccessor(obj)
if err != nil {
return "", fmt.Errorf("extract meta accessor: %w", err)
}
// Message from annotations
commitMessage := meta.GetMessage()
if commitMessage == "" {
g := meta.GetGeneration()
if g > 0 {
commitMessage = fmt.Sprintf("Generation: %d", g)
} else {
commitMessage = "exported from grafana"
}
}
name := meta.GetName()
if name == "" {
return "", ErrMissingName
}
manager, _ := meta.GetManagerProperties()
// TODO: how should we handle this?
if manager.Identity == r.repo.Config().GetName() {
// If it's already in the repository, we don't need to write it
return "", ErrAlreadyInRepository
}
title := meta.FindTitle("")
if title == "" {
title = name
}
folder := meta.GetFolder()
// Get the absolute path of the folder
rootFolder := RootFolder(r.repo.Config())
fid, ok := r.folders.Tree().DirPath(folder, rootFolder)
if !ok {
return "", fmt.Errorf("folder not found in tree: %s", folder)
}
fileName := slugify.Slugify(title) + ".json"
if fid.Path != "" {
fileName = safepath.Join(fid.Path, fileName)
}
if options.Path != "" {
fileName = safepath.Join(options.Path, fileName)
}
parsed := ParsedResource{
Info: &repository.FileInfo{
Path: fileName,
Ref: options.Ref,
},
Obj: obj,
}
body, err := parsed.ToSaveBytes()
if err != nil {
return "", err
}
err = r.repo.Write(ctx, fileName, options.Ref, body, commitMessage)
if err != nil {
return "", fmt.Errorf("failed to write file: %w", err)
}
return fileName, nil
}
func (r *ResourcesManager) WriteResourceFromFile(ctx context.Context, path string, ref string) (string, schema.GroupVersionKind, error) {
// Read the referenced file
fileInfo, err := r.repo.Read(ctx, path, ref)
if err != nil {
return "", schema.GroupVersionKind{}, fmt.Errorf("failed to read file: %w", err)
}
parsed, err := r.parser.Parse(ctx, fileInfo)
if err != nil {
return "", schema.GroupVersionKind{}, fmt.Errorf("failed to parse file: %w", err)
}
if parsed.Obj.GetName() == "" {
return "", schema.GroupVersionKind{}, ErrMissingName
}
// Check if the resource already exists
id := resourceID{
Name: parsed.Obj.GetName(),
Resource: parsed.GVR.Resource,
Group: parsed.GVK.Group,
}
existing, found := r.resourcesLookup[id]
if found {
return "", parsed.GVK, fmt.Errorf("duplicate resource name: %s, %s and %s", parsed.Obj.GetName(), path, existing)
}
r.resourcesLookup[id] = path
// For resources that exist in folders, set the header annotation
if slices.Contains(SupportsFolderAnnotation, parsed.GVR.GroupResource()) {
// Make sure the parent folders exist
folder, err := r.folders.EnsureFolderPathExist(ctx, path)
if err != nil {
return "", parsed.GVK, fmt.Errorf("failed to ensure folder path exists: %w", err)
}
parsed.Meta.SetFolder(folder)
}
// Clear any saved identifiers
parsed.Meta.SetUID("")
parsed.Meta.SetResourceVersion("")
err = parsed.Run(ctx)
return parsed.Obj.GetName(), parsed.GVK, err
}
func (r *ResourcesManager) RenameResourceFile(ctx context.Context, previousPath, previousRef, newPath, newRef string) (string, schema.GroupVersionKind, error) {
name, gvk, err := r.RemoveResourceFromFile(ctx, previousPath, previousRef)
if err != nil {
return name, gvk, fmt.Errorf("failed to remove resource: %w", err)
}
return r.WriteResourceFromFile(ctx, newPath, newRef)
}
func (r *ResourcesManager) RemoveResourceFromFile(ctx context.Context, path string, ref string) (string, schema.GroupVersionKind, error) {
info, err := r.repo.Read(ctx, path, ref)
if err != nil {
return "", schema.GroupVersionKind{}, fmt.Errorf("failed to read file: %w", err)
}
obj, gvk, _ := DecodeYAMLObject(bytes.NewBuffer(info.Data))
if obj == nil {
return "", schema.GroupVersionKind{}, fmt.Errorf("no object found")
}
objName := obj.GetName()
if objName == "" {
return "", schema.GroupVersionKind{}, ErrMissingName
}
client, _, err := r.clients.ForKind(*gvk)
if err != nil {
return "", schema.GroupVersionKind{}, fmt.Errorf("unable to get client for deleted object: %w", err)
}
err = client.Delete(ctx, objName, metav1.DeleteOptions{})
if err != nil {
return "", schema.GroupVersionKind{}, fmt.Errorf("failed to delete: %w", err)
}
return objName, schema.GroupVersionKind{}, nil
}