Remote provisioning: consolidate resource operations (#102972)
* Move to new repository * Rename it to dual writer * Rename the function * Rename the methods * Rename to exportResource * Clean up logic in migrate and add TODOs * Add TODOs * Use generic client for unprovisioned * ForEachResource * More consolidation * Refactor more around client * Consolidate constants * ForEachFolder * More use of constants * Add FIXME notes * Use more constant * Remove Dashboard * Pass tree to folder manager * Replicate tree * Reduce export complexity * More refactoring * Use the ForEach for loading users * Limit in-memory folders * Isolate the object * Improve the export function * Move resources to resources package * Move delete operation * Move more logic * More consolidation * More renaming * Fix more issues * Ensure path exists when created a resource * Simply append error * Fix receiver lint issue * Fix cyclomatic complexity * Fix linting * Remove folder path creation
This commit is contained in:
@@ -5,10 +5,13 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
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"
|
||||
|
||||
dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
iam "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
@@ -142,29 +145,105 @@ func (c *ResourceClients) ForResource(gvr schema.GroupVersionResource) (dynamic.
|
||||
return info.client, info.gvk, nil
|
||||
}
|
||||
|
||||
func (c *ResourceClients) Folder() (dynamic.ResourceInterface, error) {
|
||||
v, _, err := c.ForResource(schema.GroupVersionResource{
|
||||
Group: folders.GROUP,
|
||||
Version: folders.VERSION,
|
||||
Resource: folders.RESOURCE,
|
||||
// ForEachResource applies the function to each resource in the discovery client
|
||||
func (c *ResourceClients) ForEachResource(ctx context.Context, kind schema.GroupVersionResource, fn func(client dynamic.ResourceInterface, item *unstructured.Unstructured) error) error {
|
||||
client, _, err := c.ForResource(kind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ForEachResource(ctx, client, func(item *unstructured.Unstructured) error {
|
||||
return fn(client, item)
|
||||
})
|
||||
return v, err
|
||||
}
|
||||
|
||||
// ForEachResource applies the function to each resource in the discovery client
|
||||
func ForEachResource(ctx context.Context, client dynamic.ResourceInterface, fn func(item *unstructured.Unstructured) error) error {
|
||||
var continueToken string
|
||||
for ctx.Err() == nil {
|
||||
list, err := client.List(ctx, metav1.ListOptions{Limit: 100, Continue: continueToken})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error executing list: %w", err)
|
||||
}
|
||||
|
||||
for _, item := range list.Items {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
if err := fn(&item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
continueToken = list.GetContinue()
|
||||
if continueToken == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForEachUnmanagedResource applies the function to each unprovisioned supported resource
|
||||
func (c *ResourceClients) ForEachUnmanagedResource(ctx context.Context, fn func(client dynamic.ResourceInterface, item *unstructured.Unstructured) error) error {
|
||||
return c.ForEachSupportedResource(ctx, func(client dynamic.ResourceInterface, item *unstructured.Unstructured) error {
|
||||
meta, err := utils.MetaAccessor(item)
|
||||
if err != nil {
|
||||
return fmt.Errorf("extract meta accessor: %w", err)
|
||||
}
|
||||
|
||||
// Skip if managed
|
||||
_, ok := meta.GetManagerProperties()
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fn(client, item)
|
||||
})
|
||||
}
|
||||
|
||||
// ForEachSupportedResource applies the function to each supported resource
|
||||
func (c *ResourceClients) ForEachSupportedResource(ctx context.Context, fn func(client dynamic.ResourceInterface, item *unstructured.Unstructured) error) error {
|
||||
for _, kind := range SupportedResources {
|
||||
if err := c.ForEachResource(ctx, kind, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ResourceClients) Folder() (dynamic.ResourceInterface, error) {
|
||||
client, _, err := c.ForResource(FolderResource)
|
||||
return client, err
|
||||
}
|
||||
|
||||
func (c *ResourceClients) ForEachFolder(ctx context.Context, fn func(client dynamic.ResourceInterface, item *unstructured.Unstructured) error) error {
|
||||
return c.ForEachResource(ctx, FolderResource, fn)
|
||||
}
|
||||
|
||||
func (c *ResourceClients) User() (dynamic.ResourceInterface, error) {
|
||||
v, _, err := c.ForResource(schema.GroupVersionResource{
|
||||
Group: iam.GROUP,
|
||||
Version: iam.VERSION,
|
||||
Resource: iam.UserResourceInfo.GroupResource().Resource,
|
||||
})
|
||||
v, _, err := c.ForResource(UserResource)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (c *ResourceClients) Dashboard() (dynamic.ResourceInterface, error) {
|
||||
v, _, err := c.ForResource(schema.GroupVersionResource{
|
||||
Group: dashboard.GROUP,
|
||||
Version: dashboard.VERSION,
|
||||
Resource: dashboard.DASHBOARD_RESOURCE,
|
||||
})
|
||||
return v, err
|
||||
var UserResource = schema.GroupVersionResource{
|
||||
Group: iam.GROUP,
|
||||
Version: iam.VERSION,
|
||||
Resource: iam.UserResourceInfo.GroupResource().Resource,
|
||||
}
|
||||
|
||||
var FolderResource = schema.GroupVersionResource{
|
||||
Group: folders.GROUP,
|
||||
Version: folders.VERSION,
|
||||
Resource: folders.RESOURCE,
|
||||
}
|
||||
|
||||
var DashboardResource = schema.GroupVersionResource{
|
||||
Group: dashboard.GROUP,
|
||||
Version: dashboard.VERSION,
|
||||
Resource: dashboard.DASHBOARD_RESOURCE,
|
||||
}
|
||||
|
||||
// SupportedResources is the list of resources that are supported by provisioning
|
||||
var SupportedResources = []schema.GroupVersionResource{FolderResource, DashboardResource}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// DualReadWriter is a wrapper around a repository that can read and write resources
|
||||
// TODO: it does not support folders yet
|
||||
type DualReadWriter struct {
|
||||
repo repository.ReaderWriter
|
||||
parser *Parser
|
||||
folders *FolderManager
|
||||
}
|
||||
|
||||
func NewDualReadWriter(repo repository.ReaderWriter, parser *Parser, folders *FolderManager) *DualReadWriter {
|
||||
return &DualReadWriter{repo: repo, parser: parser, folders: folders}
|
||||
}
|
||||
|
||||
func (r *DualReadWriter) Read(ctx context.Context, path string, ref string) (*ParsedResource, error) {
|
||||
// TODO: implement this
|
||||
if safepath.IsDir(path) {
|
||||
return nil, fmt.Errorf("folder read not supported")
|
||||
}
|
||||
|
||||
info, err := r.repo.Read(ctx, path, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsed, err := r.parser.Parse(ctx, info, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// GVR will exist for anything we can actually save
|
||||
// TODO: Add known error in parser for unsupported resource
|
||||
if parsed.GVR == nil {
|
||||
if parsed.GVK != nil {
|
||||
//nolint:govet
|
||||
parsed.Errors = append(parsed.Errors, fmt.Errorf("unknown resource for Kind: %s", parsed.GVK.Kind))
|
||||
} else {
|
||||
parsed.Errors = append(parsed.Errors, fmt.Errorf("unknown resource"))
|
||||
}
|
||||
}
|
||||
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func (r *DualReadWriter) Delete(ctx context.Context, path string, ref string, message string) (*ParsedResource, error) {
|
||||
if err := repository.IsWriteAllowed(r.repo.Config(), ref); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: implement this
|
||||
if safepath.IsDir(path) {
|
||||
return nil, fmt.Errorf("folder delete not supported")
|
||||
}
|
||||
|
||||
file, err := r.repo.Read(ctx, path, ref)
|
||||
if err != nil {
|
||||
return nil, err // unable to read value
|
||||
}
|
||||
|
||||
// TODO: document in API specification
|
||||
// We can only delete parsable things
|
||||
parsed, err := r.parser.Parse(ctx, file, false)
|
||||
if err != nil {
|
||||
return nil, err // unable to read value
|
||||
}
|
||||
|
||||
parsed.Action = provisioning.ResourceActionDelete
|
||||
err = r.repo.Delete(ctx, path, ref, message)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("delete file from repository: %w", err)
|
||||
}
|
||||
|
||||
// Delete the file in the grafana database
|
||||
if ref == "" {
|
||||
err = parsed.Client.Delete(ctx, parsed.Obj.GetName(), metav1.DeleteOptions{})
|
||||
if apierrors.IsNotFound(err) {
|
||||
err = nil // ignorable
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("delete resource from storage: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return parsed, err
|
||||
}
|
||||
|
||||
// CreateFolder creates a new folder in the repository
|
||||
// FIXME: fix signature to return ParsedResource
|
||||
func (r *DualReadWriter) CreateFolder(ctx context.Context, path string, ref string, message string) (*provisioning.ResourceWrapper, error) {
|
||||
if err := repository.IsWriteAllowed(r.repo.Config(), ref); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !safepath.IsDir(path) {
|
||||
return nil, fmt.Errorf("not a folder path")
|
||||
}
|
||||
|
||||
// Now actually create the folder
|
||||
if err := r.repo.Create(ctx, path, ref, nil, message); err != nil {
|
||||
return nil, fmt.Errorf("failed to create folder: %w", err)
|
||||
}
|
||||
|
||||
cfg := r.repo.Config()
|
||||
wrap := &provisioning.ResourceWrapper{
|
||||
Path: path,
|
||||
Ref: ref,
|
||||
Repository: provisioning.ResourceRepositoryInfo{
|
||||
Type: cfg.Spec.Type,
|
||||
Namespace: cfg.Namespace,
|
||||
Name: cfg.Name,
|
||||
Title: cfg.Spec.Title,
|
||||
},
|
||||
Resource: provisioning.ResourceObjects{
|
||||
Action: provisioning.ResourceActionCreate,
|
||||
},
|
||||
}
|
||||
|
||||
if ref == "" {
|
||||
folderName, err := r.folders.EnsureFolderPathExist(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
current, err := r.folders.GetFolder(ctx, folderName)
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
return nil, err // unable to check if the folder exists
|
||||
}
|
||||
wrap.Resource.Upsert = v0alpha1.Unstructured{
|
||||
Object: current.Object,
|
||||
}
|
||||
}
|
||||
|
||||
return wrap, nil
|
||||
}
|
||||
|
||||
// CreateResource creates a new resource in the repository
|
||||
func (r *DualReadWriter) CreateResource(ctx context.Context, path string, ref string, message string, data []byte) (*ParsedResource, error) {
|
||||
if err := repository.IsWriteAllowed(r.repo.Config(), ref); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := &repository.FileInfo{
|
||||
Data: data,
|
||||
Path: path,
|
||||
Ref: ref,
|
||||
}
|
||||
|
||||
// TODO: improve parser to parse out of reader
|
||||
parsed, err := r.parser.Parse(ctx, info, true)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrUnableToReadResourceBytes) {
|
||||
return nil, apierrors.NewBadRequest("unable to read the request as a resource")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// GVR will exist for anything we can actually save
|
||||
// TODO: Add known error in parser for unsupported resource
|
||||
if parsed.GVR == nil {
|
||||
return nil, apierrors.NewBadRequest("The payload does not map to a known resource")
|
||||
}
|
||||
|
||||
// Do not write if any errors exist
|
||||
if len(parsed.Errors) > 0 {
|
||||
return parsed, err
|
||||
}
|
||||
|
||||
data, err = parsed.ToSaveBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.repo.Create(ctx, path, ref, data, message)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create resource in repository: %w", err)
|
||||
}
|
||||
|
||||
// Directly update the grafana database
|
||||
// Behaves the same running sync after writing
|
||||
if ref == "" {
|
||||
// FIXME: we are not creating the folder path
|
||||
// TODO: will existing also be present here? for update?
|
||||
if parsed.Existing == nil {
|
||||
parsed.Upsert, err = parsed.Client.Create(ctx, parsed.Obj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
parsed.Errors = append(parsed.Errors, err)
|
||||
}
|
||||
} else {
|
||||
parsed.Upsert, err = parsed.Client.Update(ctx, parsed.Obj, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
parsed.Errors = append(parsed.Errors, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsed, err
|
||||
}
|
||||
|
||||
// UpdateResource updates a resource in the repository
|
||||
func (r *DualReadWriter) UpdateResource(ctx context.Context, path string, ref string, message string, data []byte) (*ParsedResource, error) {
|
||||
if err := repository.IsWriteAllowed(r.repo.Config(), ref); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := &repository.FileInfo{
|
||||
Data: data,
|
||||
Path: path,
|
||||
Ref: ref,
|
||||
}
|
||||
|
||||
// TODO: improve parser to parse out of reader
|
||||
parsed, err := r.parser.Parse(ctx, info, true)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrUnableToReadResourceBytes) {
|
||||
return nil, apierrors.NewBadRequest("unable to read the request as a resource")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// GVR will exist for anything we can actually save
|
||||
// TODO: Add known error in parser for unsupported resource
|
||||
if parsed.GVR == nil {
|
||||
return nil, apierrors.NewBadRequest("The payload does not map to a known resource")
|
||||
}
|
||||
|
||||
// Do not write if any errors exist
|
||||
if len(parsed.Errors) > 0 {
|
||||
return parsed, err
|
||||
}
|
||||
|
||||
data, err = parsed.ToSaveBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.repo.Update(ctx, path, ref, data, message)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update resource in repository: %w", err)
|
||||
}
|
||||
|
||||
// Directly update the grafana database
|
||||
// Behaves the same running sync after writing
|
||||
if ref == "" {
|
||||
// FIXME: we are not creating the folder path
|
||||
// FIXME: I don't like this parsed strategy here
|
||||
if parsed.Existing == nil {
|
||||
parsed.Upsert, err = parsed.Client.Create(ctx, parsed.Obj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
parsed.Errors = append(parsed.Errors, err)
|
||||
}
|
||||
} else {
|
||||
parsed.Upsert, err = parsed.Client.Update(ctx, parsed.Obj, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
parsed.Errors = append(parsed.Errors, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsed, err
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -15,16 +16,18 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
|
||||
)
|
||||
|
||||
const maxFolders = 10000
|
||||
|
||||
type FolderManager struct {
|
||||
repo repository.Repository
|
||||
lookup *FolderTree
|
||||
repo repository.ReaderWriter
|
||||
tree *FolderTree
|
||||
client dynamic.ResourceInterface
|
||||
}
|
||||
|
||||
func NewFolderManager(repo repository.Repository, client dynamic.ResourceInterface) *FolderManager {
|
||||
func NewFolderManager(repo repository.ReaderWriter, client dynamic.ResourceInterface, lookup *FolderTree) *FolderManager {
|
||||
return &FolderManager{
|
||||
repo: repo,
|
||||
lookup: NewEmptyFolderTree(),
|
||||
tree: lookup,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
@@ -33,6 +36,14 @@ func (fm *FolderManager) Client() dynamic.ResourceInterface {
|
||||
return fm.client
|
||||
}
|
||||
|
||||
func (fm *FolderManager) Tree() *FolderTree {
|
||||
return fm.tree
|
||||
}
|
||||
|
||||
func (fm *FolderManager) SetTree(tree *FolderTree) {
|
||||
fm.tree = tree
|
||||
}
|
||||
|
||||
// EnsureFoldersExist creates the folder structure in the cluster.
|
||||
func (fm *FolderManager) EnsureFolderPathExist(ctx context.Context, filePath string) (parent string, err error) {
|
||||
cfg := fm.repo.Config()
|
||||
@@ -48,13 +59,13 @@ func (fm *FolderManager) EnsureFolderPathExist(ctx context.Context, filePath str
|
||||
}
|
||||
|
||||
f := ParseFolder(dir, cfg.Name)
|
||||
if fm.lookup.In(f.ID) {
|
||||
if fm.tree.In(f.ID) {
|
||||
return f.ID, nil
|
||||
}
|
||||
|
||||
err = safepath.Walk(ctx, f.Path, func(ctx context.Context, traverse string) error {
|
||||
f := ParseFolder(traverse, cfg.GetName())
|
||||
if fm.lookup.In(f.ID) {
|
||||
if fm.tree.In(f.ID) {
|
||||
parent = f.ID
|
||||
return nil
|
||||
}
|
||||
@@ -63,7 +74,7 @@ func (fm *FolderManager) EnsureFolderPathExist(ctx context.Context, filePath str
|
||||
return fmt.Errorf("ensure folder exists: %w", err)
|
||||
}
|
||||
|
||||
fm.lookup.Add(f, parent)
|
||||
fm.tree.Add(f, parent)
|
||||
parent = f.ID
|
||||
return nil
|
||||
})
|
||||
@@ -131,3 +142,40 @@ func (fm *FolderManager) EnsureFolderExists(ctx context.Context, folder Folder,
|
||||
func (fm *FolderManager) GetFolder(ctx context.Context, name string) (*unstructured.Unstructured, error) {
|
||||
return fm.client.Get(ctx, name, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
// ReplicateTree replicates the folder tree to the repository.
|
||||
// The function fn is called for each folder.
|
||||
// If the folder already exists, the function is called with created set to false.
|
||||
// If the folder is created, the function is called with created set to true.
|
||||
func (fm *FolderManager) EnsureTreeExists(ctx context.Context, ref, path string, fn func(folder Folder, created bool, err error) error) error {
|
||||
return fm.tree.Walk(ctx, func(ctx context.Context, folder Folder) error {
|
||||
p := folder.Path
|
||||
if path != "" {
|
||||
p = safepath.Join(path, p)
|
||||
}
|
||||
|
||||
_, err := fm.repo.Read(ctx, p, ref)
|
||||
if err != nil && !(errors.Is(err, repository.ErrFileNotFound) || apierrors.IsNotFound(err)) {
|
||||
return fn(folder, false, fmt.Errorf("check if folder exists before writing: %w", err))
|
||||
} else if err == nil {
|
||||
return fn(folder, false, nil)
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("Add folder %s", p)
|
||||
if err := fm.repo.Create(ctx, p, ref, nil, msg); err != nil {
|
||||
return fn(folder, true, fmt.Errorf("write folder in repo: %w", err))
|
||||
}
|
||||
|
||||
return fn(folder, true, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func (fm *FolderManager) LoadFromServer(ctx context.Context) error {
|
||||
return ForEachResource(ctx, fm.client, func(item *unstructured.Unstructured) error {
|
||||
if fm.tree.Count() > maxFolders {
|
||||
return errors.New("too many folders")
|
||||
}
|
||||
|
||||
return fm.tree.AddUnstructured(item, fm.repo.Config().Name)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,6 +104,8 @@ type ParsedResource struct {
|
||||
Errors []error
|
||||
}
|
||||
|
||||
// FIXME: eliminate clients from parser
|
||||
|
||||
func (r *Parser) Clients() *ResourceClients {
|
||||
return r.clients
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var ErrAlreadyInRepository = errors.New("already in repository")
|
||||
|
||||
type WriteOptions struct {
|
||||
Identifier bool
|
||||
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
|
||||
userInfo map[string]repository.CommitSignature
|
||||
resourcesLookup map[resourceID]string // the path with this k8s name
|
||||
}
|
||||
|
||||
func NewResourcesManager(repo repository.ReaderWriter, folders *FolderManager, parser *Parser, clients *ResourceClients, userInfo map[string]repository.CommitSignature) *ResourcesManager {
|
||||
return &ResourcesManager{
|
||||
repo: repo,
|
||||
folders: folders,
|
||||
parser: parser,
|
||||
clients: clients,
|
||||
userInfo: userInfo,
|
||||
resourcesLookup: map[resourceID]string{},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateResource writes an object to the repository
|
||||
func (r *ResourcesManager) CreateResourceFileFromObject(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"
|
||||
}
|
||||
}
|
||||
|
||||
ctx = r.withAuthorSignature(ctx, meta)
|
||||
|
||||
name := meta.GetName()
|
||||
manager, _ := meta.GetManagerProperties()
|
||||
// TODO: how we should 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
|
||||
fid, ok := r.folders.Tree().DirPath(folder, "")
|
||||
if !ok {
|
||||
// FIXME: Shouldn't this fail instead?
|
||||
fid = Folder{
|
||||
Path: "__folder_not_found/" + slugify.Slugify(folder),
|
||||
}
|
||||
// r.logger.Error("folder of item was not in tree of repository")
|
||||
}
|
||||
|
||||
// Clear the metadata
|
||||
delete(obj.Object, "metadata")
|
||||
|
||||
if options.Identifier {
|
||||
meta.SetName(name) // keep the identifier in the metadata
|
||||
}
|
||||
|
||||
body, err := json.MarshalIndent(obj.Object, "", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal dashboard: %w", err)
|
||||
}
|
||||
|
||||
fileName := slugify.Slugify(title) + ".json"
|
||||
if fid.Path != "" {
|
||||
fileName = safepath.Join(fid.Path, fileName)
|
||||
}
|
||||
if options.Path != "" {
|
||||
fileName = safepath.Join(options.Path, fileName)
|
||||
}
|
||||
|
||||
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 "", nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
parsed, err := r.parser.Parse(ctx, fileInfo, false) // no validation
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to parse file: %w", err)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// 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)
|
||||
parsed.Meta.SetUID("") // clear identifiers
|
||||
parsed.Meta.SetResourceVersion("") // clear identifiers
|
||||
|
||||
// Update will also create (for resources we care about)
|
||||
_, err = parsed.Client.Update(ctx, parsed.Obj, metav1.UpdateOptions{})
|
||||
|
||||
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 "", nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
obj, gvk, _ := DecodeYAMLObject(bytes.NewBuffer(info.Data))
|
||||
if obj == nil {
|
||||
return "", nil, fmt.Errorf("no object found")
|
||||
}
|
||||
|
||||
objName := obj.GetName()
|
||||
if objName == "" {
|
||||
// Find the referenced file
|
||||
objName, _ = NamesFromHashedRepoPath(r.repo.Config().Name, path)
|
||||
}
|
||||
|
||||
client, _, err := r.clients.ForKind(*gvk)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("unable to get client for deleted object: %w", err)
|
||||
}
|
||||
|
||||
err = client.Delete(ctx, objName, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to delete: %w", err)
|
||||
}
|
||||
|
||||
return objName, gvk, nil
|
||||
}
|
||||
|
||||
func (r *ResourcesManager) withAuthorSignature(ctx context.Context, item utils.GrafanaMetaAccessor) context.Context {
|
||||
id := item.GetUpdatedBy()
|
||||
if id == "" {
|
||||
id = item.GetCreatedBy()
|
||||
}
|
||||
if id == "" {
|
||||
id = "grafana"
|
||||
}
|
||||
|
||||
sig := r.userInfo[id] // lookup
|
||||
if sig.Name == "" && sig.Email == "" {
|
||||
sig.Name = id
|
||||
}
|
||||
t, err := item.GetUpdatedTimestamp()
|
||||
if err == nil && t != nil {
|
||||
sig.When = *t
|
||||
} else {
|
||||
sig.When = item.GetCreationTimestamp().Time
|
||||
}
|
||||
|
||||
return repository.WithAuthorSignature(ctx, sig)
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
type FolderTree struct {
|
||||
tree map[string]string
|
||||
folders map[string]Folder
|
||||
count int
|
||||
}
|
||||
|
||||
// In determines if the given folder is in the tree at all. That is, it answers "does the folder even exist in the Grafana instance?"
|
||||
@@ -66,6 +67,11 @@ func (t *FolderTree) DirPath(folder, baseFolder string) (fid Folder, ok bool) {
|
||||
func (t *FolderTree) Add(folder Folder, parent string) {
|
||||
t.tree[folder.ID] = parent
|
||||
t.folders[folder.ID] = folder
|
||||
t.count++
|
||||
}
|
||||
|
||||
func (t *FolderTree) Count() int {
|
||||
return t.count
|
||||
}
|
||||
|
||||
type WalkFunc func(ctx context.Context, folder Folder) error
|
||||
@@ -113,6 +119,7 @@ func (t *FolderTree) AddUnstructured(item *unstructured.Unstructured, skipRepo s
|
||||
}
|
||||
t.tree[folder.ID] = meta.GetFolder()
|
||||
t.folders[folder.ID] = folder
|
||||
t.count++
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -133,7 +140,8 @@ func NewFolderTreeFromResourceList(resources *provisioning.ResourceList) *Folder
|
||||
}
|
||||
|
||||
return &FolderTree{
|
||||
tree,
|
||||
folderIDs,
|
||||
tree: tree,
|
||||
folders: folderIDs,
|
||||
count: len(resources.Items),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user