Files
grafana/pkg/registry/apis/provisioning/jobs/sync/changes.go

154 lines
4.2 KiB
Go

package sync
import (
"context"
"fmt"
"strings"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
"github.com/grafana/grafana/apps/provisioning/pkg/safepath"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
)
type ResourceFileChange struct {
// Path to the file in a repository with a change
Path string
Action repository.FileAction
// The current value in the database -- only required for delete
Existing *provisioning.ResourceListItem
}
func Compare(ctx context.Context, repo repository.Reader, repositoryResources resources.RepositoryResources, ref string) ([]ResourceFileChange, error) {
target, err := repositoryResources.List(ctx)
if err != nil {
return nil, fmt.Errorf("error listing current: %w", err)
}
source, err := repo.ReadTree(ctx, ref)
if err != nil {
return nil, fmt.Errorf("error reading tree: %w", err)
}
changes, err := Changes(source, target)
if err != nil {
return nil, fmt.Errorf("calculate changes: %w", err)
}
if len(changes) > 0 {
// FIXME: this is a way to load in different ways the resources
// maybe we can structure the code in better way to avoid this
repositoryResources.SetTree(resources.NewFolderTreeFromResourceList(target))
}
return changes, nil
}
func Changes(source []repository.FileTreeEntry, target *provisioning.ResourceList) ([]ResourceFileChange, error) {
lookup := make(map[string]*provisioning.ResourceListItem, len(target.Items))
for _, item := range target.Items {
if item.Path == "" {
if item.Group != resources.FolderResource.Group {
return nil, fmt.Errorf("empty path on a non folder")
}
continue
}
// TODO: why do we have to do this here?
if item.Group == resources.FolderResource.Group && !strings.HasSuffix(item.Path, "/") {
item.Path = item.Path + "/"
}
lookup[item.Path] = &item
}
keep := safepath.NewTrie()
changes := make([]ResourceFileChange, 0, len(source))
for _, file := range source {
// TODO: why do we have to do this here?
if !file.Blob && !strings.HasSuffix(file.Path, "/") {
file.Path = file.Path + "/"
}
check, ok := lookup[file.Path]
if ok {
if check.Hash != file.Hash && check.Resource != resources.FolderResource.Resource {
changes = append(changes, ResourceFileChange{
Action: repository.FileActionUpdated,
Path: check.Path,
Existing: check,
})
}
if err := keep.Add(file.Path); err != nil {
return nil, fmt.Errorf("failed to add path to keep trie: %w", err)
}
if check.Resource != resources.FolderResource.Resource {
delete(lookup, file.Path)
}
continue
}
if resources.IsPathSupported(file.Path) == nil {
changes = append(changes, ResourceFileChange{
Action: repository.FileActionCreated, // or previously ignored/failed
Path: file.Path,
})
if err := keep.Add(file.Path); err != nil {
return nil, fmt.Errorf("failed to add path to keep trie: %w", err)
}
continue
}
// Maintain the safe segment for empty folders
safeSegment := safepath.SafeSegment(file.Path)
if !safepath.IsDir(safeSegment) {
safeSegment = safepath.Dir(safeSegment)
}
if safeSegment != "" && resources.IsPathSupported(safeSegment) == nil {
if err := keep.Add(safeSegment); err != nil {
return nil, fmt.Errorf("failed to add path to keep trie: %w", err)
}
_, ok := lookup[safeSegment]
if ok {
continue
}
// Ignore file change for `.keep` folders
if strings.HasSuffix(file.Path, ".keep") {
continue
}
changes = append(changes, ResourceFileChange{
Action: repository.FileActionCreated, // or previously ignored/failed
Path: safeSegment,
})
}
}
// Paths found in grafana, without a matching path in the repository
for _, v := range lookup {
if v.Resource == resources.FolderResource.Resource && keep.Exists(v.Path) {
continue
}
changes = append(changes, ResourceFileChange{
Action: repository.FileActionDeleted,
Path: v.Path,
Existing: v,
})
}
// Deepest first (stable sort order)
safepath.SortByDepth(changes, func(c ResourceFileChange) string { return c.Path }, false)
return changes, nil
}