feat: parallel processChange (#112198)
* feat: parallel processChange * all except move work * fix: tests and order of operations * fix: tests * chore: review feedback * chore: review feedback
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"gopkg.in/yaml.v3"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@@ -302,10 +304,15 @@ func (f *ParsedResource) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Always use the provisioning identity when writing
|
||||
ctx, _, err := identity.WithProvisioningIdentity(ctx, f.Obj.GetNamespace())
|
||||
identityCtx, _, err := identity.WithProvisioningIdentity(ctx, f.Obj.GetNamespace())
|
||||
ctx, identitySpan := tracing.Start(identityCtx, "provisioning.resources.run_resource.set_identity")
|
||||
|
||||
if err != nil {
|
||||
identitySpan.RecordError(err)
|
||||
identitySpan.End()
|
||||
return err
|
||||
}
|
||||
identitySpan.End()
|
||||
|
||||
fieldValidation := "Strict"
|
||||
if f.GVR == DashboardResource {
|
||||
@@ -318,42 +325,57 @@ func (f *ParsedResource) Run(ctx context.Context) error {
|
||||
Identity: f.Repo.Name,
|
||||
}
|
||||
|
||||
actionsCtx, actionsSpan := tracing.Start(ctx, "provisioning.resources.run_resource.actions")
|
||||
defer actionsSpan.End()
|
||||
|
||||
// Handle deletion action
|
||||
if f.Action == provisioning.ResourceActionDelete {
|
||||
deleteCtx, deleteSpan := tracing.Start(actionsCtx, "provisioning.resources.run_resource.delete")
|
||||
deleteSpan.SetAttributes(attribute.String("resource.name", f.Obj.GetName()))
|
||||
|
||||
// If we don't have existing resource from DryRun, fetch it now
|
||||
if f.DryRunResponse == nil {
|
||||
f.Existing, err = f.Client.Get(ctx, f.Obj.GetName(), metav1.GetOptions{})
|
||||
f.Existing, err = f.Client.Get(deleteCtx, f.Obj.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
deleteSpan.RecordError(err)
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Resource doesn't exist, nothing to delete - this is fine
|
||||
deleteSpan.End()
|
||||
return nil
|
||||
}
|
||||
deleteSpan.End()
|
||||
return fmt.Errorf("failed to get existing resource for delete: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check ownership with the existing resource
|
||||
if err := CheckResourceOwnership(f.Existing, f.Obj.GetName(), requestingManager); err != nil {
|
||||
deleteSpan.RecordError(err)
|
||||
deleteSpan.End()
|
||||
return err
|
||||
}
|
||||
|
||||
// Perform the actual delete
|
||||
err = f.Client.Delete(ctx, f.Obj.GetName(), metav1.DeleteOptions{})
|
||||
err = f.Client.Delete(deleteCtx, f.Obj.GetName(), metav1.DeleteOptions{})
|
||||
if apierrors.IsNotFound(err) {
|
||||
err = nil // ignorable - resource was already deleted
|
||||
}
|
||||
if err != nil {
|
||||
deleteSpan.RecordError(err)
|
||||
}
|
||||
|
||||
// Set the deleted resource as the result
|
||||
if err == nil && f.Existing != nil {
|
||||
f.Upsert = f.Existing.DeepCopy()
|
||||
}
|
||||
|
||||
deleteSpan.End()
|
||||
return err
|
||||
}
|
||||
|
||||
// If we don't have existing resource from DryRun, fetch it now
|
||||
if f.DryRunResponse == nil {
|
||||
f.Existing, _ = f.Client.Get(ctx, f.Obj.GetName(), metav1.GetOptions{})
|
||||
f.Existing, _ = f.Client.Get(actionsCtx, f.Obj.GetName(), metav1.GetOptions{})
|
||||
}
|
||||
|
||||
// Check ownership with the existing resource (if any)
|
||||
@@ -364,9 +386,16 @@ func (f *ParsedResource) Run(ctx context.Context) error {
|
||||
// If we have already tried loading existing, start with create
|
||||
if f.DryRunResponse != nil && f.Existing == nil {
|
||||
f.Action = provisioning.ResourceActionCreate
|
||||
f.Upsert, err = f.Client.Create(ctx, f.Obj, metav1.CreateOptions{
|
||||
createCtx, createSpan := tracing.Start(actionsCtx, "provisioning.resources.run_resource.create")
|
||||
createSpan.SetAttributes(attribute.String("resource.name", f.Obj.GetName()))
|
||||
f.Upsert, err = f.Client.Create(createCtx, f.Obj, metav1.CreateOptions{
|
||||
FieldValidation: fieldValidation,
|
||||
})
|
||||
if err != nil {
|
||||
createSpan.RecordError(err)
|
||||
}
|
||||
createSpan.End()
|
||||
|
||||
if err == nil {
|
||||
return nil // it worked, return
|
||||
}
|
||||
@@ -374,14 +403,28 @@ func (f *ParsedResource) Run(ctx context.Context) error {
|
||||
|
||||
// Try update, otherwise create
|
||||
f.Action = provisioning.ResourceActionUpdate
|
||||
f.Upsert, err = f.Client.Update(ctx, f.Obj, metav1.UpdateOptions{
|
||||
|
||||
updateCtx, updateSpan := tracing.Start(actionsCtx, "provisioning.resources.run_resource.update")
|
||||
updateSpan.SetAttributes(attribute.String("resource.name", f.Obj.GetName()))
|
||||
f.Upsert, err = f.Client.Update(updateCtx, f.Obj, metav1.UpdateOptions{
|
||||
FieldValidation: fieldValidation,
|
||||
})
|
||||
if err != nil {
|
||||
updateSpan.RecordError(err)
|
||||
}
|
||||
updateSpan.End()
|
||||
|
||||
if apierrors.IsNotFound(err) {
|
||||
f.Action = provisioning.ResourceActionCreate
|
||||
f.Upsert, err = f.Client.Create(ctx, f.Obj, metav1.CreateOptions{
|
||||
fallbackCreateCtx, fallbackCreateSpan := tracing.Start(actionsCtx, "provisioning.resources.run_resource.create_fallback")
|
||||
fallbackCreateSpan.SetAttributes(attribute.String("resource.name", f.Obj.GetName()))
|
||||
f.Upsert, err = f.Client.Create(fallbackCreateCtx, f.Obj, metav1.CreateOptions{
|
||||
FieldValidation: fieldValidation,
|
||||
})
|
||||
if err != nil {
|
||||
fallbackCreateSpan.RecordError(err)
|
||||
}
|
||||
fallbackCreateSpan.End()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/safepath"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/infra/slugify"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -55,6 +57,7 @@ type ResourcesManager struct {
|
||||
parser Parser
|
||||
clients ResourceClients
|
||||
resourcesLookup map[resourceID]string // the path with this k8s name
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewResourcesManager(repo repository.ReaderWriter, folders *FolderManager, parser Parser, clients ResourceClients) *ResourcesManager {
|
||||
@@ -67,6 +70,25 @@ func NewResourcesManager(repo repository.ReaderWriter, folders *FolderManager, p
|
||||
}
|
||||
}
|
||||
|
||||
// findResource checks if a resource exists in the lookup map (read operation)
|
||||
func (r *ResourcesManager) findResource(id resourceID) (string, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
path, found := r.resourcesLookup[id]
|
||||
return path, found
|
||||
}
|
||||
|
||||
func (r *ResourcesManager) addResource(id resourceID, path string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, found := r.resourcesLookup[id]; found {
|
||||
return
|
||||
}
|
||||
|
||||
r.resourcesLookup[id] = path
|
||||
}
|
||||
|
||||
// CheckResourceOwnership validates that the requesting manager can modify the existing resource
|
||||
// Returns an error if the existing resource is owned by a different manager that doesn't allow edits
|
||||
// If existingResource is nil, no ownership conflict exists (new resource)
|
||||
@@ -193,15 +215,23 @@ func (r *ResourcesManager) WriteResourceFileFromObject(ctx context.Context, obj
|
||||
|
||||
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)
|
||||
readCtx, readSpan := tracing.Start(ctx, "provisioning.resources.write_resource_from_file.read_file")
|
||||
fileInfo, err := r.repo.Read(readCtx, path, ref)
|
||||
if err != nil {
|
||||
readSpan.RecordError(err)
|
||||
readSpan.End()
|
||||
return "", schema.GroupVersionKind{}, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
readSpan.End()
|
||||
|
||||
parsed, err := r.parser.Parse(ctx, fileInfo)
|
||||
parseCtx, parseSpan := tracing.Start(ctx, "provisioning.resources.write_resource_from_file.parse_file")
|
||||
parsed, err := r.parser.Parse(parseCtx, fileInfo)
|
||||
if err != nil {
|
||||
parseSpan.RecordError(err)
|
||||
parseSpan.End()
|
||||
return "", schema.GroupVersionKind{}, fmt.Errorf("failed to parse file: %w", err)
|
||||
}
|
||||
parseSpan.End()
|
||||
|
||||
if parsed.Obj.GetName() == "" {
|
||||
return "", schema.GroupVersionKind{}, ErrMissingName
|
||||
@@ -213,27 +243,36 @@ func (r *ResourcesManager) WriteResourceFromFile(ctx context.Context, path strin
|
||||
Resource: parsed.GVR.Resource,
|
||||
Group: parsed.GVK.Group,
|
||||
}
|
||||
existing, found := r.resourcesLookup[id]
|
||||
if found {
|
||||
|
||||
if existing, found := r.findResource(id); found {
|
||||
return "", parsed.GVK, fmt.Errorf("duplicate resource name: %s, %s and %s: %w", parsed.Obj.GetName(), path, existing, ErrDuplicateName)
|
||||
}
|
||||
r.resourcesLookup[id] = path
|
||||
r.addResource(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)
|
||||
folderCtx, folderSpan := tracing.Start(ctx, "provisioning.resources.write_resource_from_file.ensure_folder")
|
||||
folder, err := r.folders.EnsureFolderPathExist(folderCtx, path)
|
||||
if err != nil {
|
||||
folderSpan.RecordError(err)
|
||||
folderSpan.End()
|
||||
return "", parsed.GVK, fmt.Errorf("failed to ensure folder path exists: %w", err)
|
||||
}
|
||||
parsed.Meta.SetFolder(folder)
|
||||
folderSpan.End()
|
||||
}
|
||||
|
||||
// Clear any saved identifiers
|
||||
parsed.Meta.SetUID("")
|
||||
parsed.Meta.SetResourceVersion("")
|
||||
|
||||
err = parsed.Run(ctx)
|
||||
runCtx, runSpan := tracing.Start(ctx, "provisioning.resources.write_resource_from_file.run_resource")
|
||||
err = parsed.Run(runCtx)
|
||||
if err != nil {
|
||||
runSpan.RecordError(err)
|
||||
}
|
||||
runSpan.End()
|
||||
|
||||
return parsed.Obj.GetName(), parsed.GVK, err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package resources
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
@@ -30,11 +31,18 @@ type folderTree struct {
|
||||
tree map[string]string
|
||||
folders map[string]Folder
|
||||
count int
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// 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?"
|
||||
// An empty folder string means the root folder, and is special-cased to always return true.
|
||||
func (t *folderTree) In(folder string) bool {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
return t.in(folder)
|
||||
}
|
||||
|
||||
func (t *folderTree) in(folder string) bool {
|
||||
_, ok := t.tree[folder]
|
||||
return ok || folder == ""
|
||||
}
|
||||
@@ -47,7 +55,19 @@ func (t *folderTree) In(folder string) bool {
|
||||
// If In(folder) or In(baseFolder) is false, this will return ok=false, because it would be undefined behaviour.
|
||||
// If baseFolder is not a parent of folder, ok=false is returned.
|
||||
func (t *folderTree) DirPath(folder, baseFolder string) (fid Folder, ok bool) {
|
||||
if !t.In(folder) || !t.In(baseFolder) {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
return t.dirPath(folder, baseFolder)
|
||||
}
|
||||
|
||||
// dirPath is the internal implementation that assumes the mutex is already held
|
||||
// Needed to avoid deadlock when called from other methods that hold locks like Walk()
|
||||
func (t *folderTree) dirPath(folder, baseFolder string) (fid Folder, ok bool) {
|
||||
// Inline In() logic to avoid deadlock when called from other methods that hold locks
|
||||
folderInTree := t.in(folder)
|
||||
baseFolderInTree := t.in(baseFolder)
|
||||
|
||||
if !folderInTree || !baseFolderInTree {
|
||||
return Folder{}, false
|
||||
}
|
||||
if folder == "" && baseFolder != "" {
|
||||
@@ -76,21 +96,27 @@ func (t *folderTree) DirPath(folder, baseFolder string) (fid Folder, ok bool) {
|
||||
}
|
||||
|
||||
func (t *folderTree) Add(folder Folder, parent string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.tree[folder.ID] = parent
|
||||
t.folders[folder.ID] = folder
|
||||
t.count++
|
||||
}
|
||||
|
||||
func (t *folderTree) Count() int {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
return t.count
|
||||
}
|
||||
|
||||
type WalkFunc func(ctx context.Context, folder Folder, parent string) error
|
||||
|
||||
func (t *folderTree) Walk(ctx context.Context, fn WalkFunc) error {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
toWalk := make([]Folder, 0, len(t.folders))
|
||||
for _, folder := range t.folders {
|
||||
folder, _ := t.DirPath(folder.ID, "")
|
||||
folder, _ := t.dirPath(folder.ID, "")
|
||||
toWalk = append(toWalk, folder)
|
||||
}
|
||||
|
||||
@@ -123,6 +149,8 @@ func (t *folderTree) AddUnstructured(item *unstructured.Unstructured) error {
|
||||
Title: meta.FindTitle(item.GetName()),
|
||||
ID: item.GetName(),
|
||||
}
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.tree[folder.ID] = meta.GetFolder()
|
||||
t.folders[folder.ID] = folder
|
||||
t.count++
|
||||
|
||||
Reference in New Issue
Block a user