* client and repo info * set repo header info * add ref to header info * add comment --------- Co-authored-by: Mariell Hoversholm <mariell.hoversholm@grafana.com>
201 lines
5.0 KiB
Go
201 lines
5.0 KiB
Go
package resources
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
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"
|
|
|
|
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
|
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
|
)
|
|
|
|
var ErrNamespaceMismatch = errors.New("the file namespace does not match target namespace")
|
|
|
|
type FileParser struct {
|
|
// The target repository
|
|
repo repository.Repository
|
|
|
|
// client helper (for this namespace?)
|
|
client *DynamicClient
|
|
kinds *KindsLookup
|
|
}
|
|
|
|
func NewParser(repo repository.Repository, client *DynamicClient, kinds *KindsLookup) *FileParser {
|
|
return &FileParser{
|
|
repo: repo,
|
|
client: client,
|
|
kinds: kinds,
|
|
}
|
|
}
|
|
|
|
type ParsedFile struct {
|
|
// Original file Info
|
|
Info *repository.FileInfo
|
|
// Parsed contents
|
|
Obj *unstructured.Unstructured
|
|
// Metadata accessor for the file object
|
|
Meta utils.GrafanaMetaAccessor
|
|
|
|
// The Kind is defined in the file
|
|
GVK *schema.GroupVersionKind
|
|
// The Resource is found by mapping Kind to the right apiserver
|
|
GVR *schema.GroupVersionResource
|
|
// Client that can talk to this resource
|
|
Client dynamic.ResourceInterface
|
|
|
|
// The Existing object (same name)
|
|
// ?? do we need/want the whole thing??
|
|
Existing *unstructured.Unstructured
|
|
|
|
// The results from dry run
|
|
DryRunResponse *unstructured.Unstructured
|
|
|
|
// If we got some Errors
|
|
Errors []error
|
|
}
|
|
|
|
func (r *FileParser) Parse(ctx context.Context, logger *slog.Logger, info *repository.FileInfo, validate bool) (*ParsedFile, error) {
|
|
obj, gvk, err := LoadYAMLOrJSON(bytes.NewBuffer(info.Data))
|
|
if err != nil {
|
|
logger.DebugContext(ctx, "failed to find GVK of the input data", "error", err)
|
|
obj, gvk, err = FallbackResourceLoader(ctx, logger, info.Data)
|
|
if err != nil {
|
|
logger.DebugContext(ctx, "also failed to get GVK from fallback loader?", "error", err)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
meta, err := utils.MetaAccessor(obj)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parsed := &ParsedFile{
|
|
Info: info,
|
|
Obj: obj,
|
|
Meta: meta,
|
|
GVK: gvk,
|
|
}
|
|
|
|
// Validate the namespace
|
|
if obj.GetNamespace() != "" && obj.GetNamespace() != r.client.GetNamespace() {
|
|
parsed.Errors = append(parsed.Errors, ErrNamespaceMismatch)
|
|
}
|
|
|
|
obj.SetNamespace(r.client.GetNamespace())
|
|
meta.SetRepositoryInfo(&utils.ResourceRepositoryInfo{
|
|
Name: r.repo.Config().Name,
|
|
Path: joinPathWithRef(info.Path, info.Ref),
|
|
Hash: info.Hash,
|
|
Timestamp: nil, // ???&info.Modified.Time,
|
|
})
|
|
|
|
// When name is missing use the file path as the k8s name
|
|
if obj.GetName() == "" {
|
|
name := filepath.Base(info.Path)
|
|
suffix := filepath.Ext(name)
|
|
if suffix != "" {
|
|
name = strings.TrimSuffix(name, suffix)
|
|
}
|
|
obj.SetName(name)
|
|
}
|
|
|
|
// We can not do anything more if no kind is defined
|
|
if gvk == nil {
|
|
return parsed, nil
|
|
}
|
|
|
|
gvr, ok := r.kinds.Resource(*gvk)
|
|
if !ok {
|
|
return parsed, nil
|
|
}
|
|
|
|
client := r.client.Resource(gvr)
|
|
parsed.GVR = &gvr
|
|
parsed.Client = client
|
|
if !validate {
|
|
return parsed, nil
|
|
}
|
|
|
|
// Dry run CREATE or UPDATE
|
|
parsed.Existing, _ = parsed.Client.Get(ctx, obj.GetName(), metav1.GetOptions{})
|
|
if parsed.Existing == nil {
|
|
parsed.DryRunResponse, err = parsed.Client.Create(ctx, obj, metav1.CreateOptions{
|
|
DryRun: []string{"All"},
|
|
})
|
|
} else {
|
|
parsed.DryRunResponse, err = parsed.Client.Update(ctx, obj, metav1.UpdateOptions{
|
|
DryRun: []string{"All"},
|
|
})
|
|
}
|
|
if err != nil {
|
|
parsed.Errors = append(parsed.Errors, err)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func (f *ParsedFile) ToSaveBytes() ([]byte, error) {
|
|
// TODO... should use the validated one?
|
|
obj := f.Obj.Object
|
|
delete(obj, "metadata")
|
|
|
|
switch filepath.Ext(f.Info.Path) {
|
|
// JSON pretty print
|
|
case ".json":
|
|
return json.MarshalIndent(obj, "", " ")
|
|
|
|
// Write the value as yaml
|
|
case ".yaml", ".yml":
|
|
return yaml.Marshal(obj)
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unexpected format")
|
|
}
|
|
}
|
|
|
|
func (f *ParsedFile) AsResourceWrapper() *provisioning.ResourceWrapper {
|
|
info := f.Info
|
|
res := provisioning.ResourceObjects{}
|
|
if f.Obj != nil {
|
|
res.File = v0alpha1.Unstructured{Object: f.Obj.Object}
|
|
}
|
|
if f.Existing != nil {
|
|
res.Store = v0alpha1.Unstructured{Object: f.Existing.Object}
|
|
}
|
|
if f.DryRunResponse != nil {
|
|
res.DryRun = v0alpha1.Unstructured{Object: f.DryRunResponse.Object}
|
|
}
|
|
wrap := &provisioning.ResourceWrapper{
|
|
Path: info.Path,
|
|
Ref: info.Ref,
|
|
Hash: info.Hash,
|
|
Timestamp: info.Modified,
|
|
Resource: res,
|
|
}
|
|
for _, err := range f.Errors {
|
|
wrap.Errors = append(wrap.Errors, err.Error())
|
|
}
|
|
return wrap
|
|
}
|
|
|
|
// Matches the frontend logic that pulls ref from the path
|
|
// public/app/features/dashboard-scene/saving/SaveProvisionedDashboard.tsx#L32
|
|
func joinPathWithRef(p, r string) string {
|
|
if r == "" {
|
|
return p
|
|
}
|
|
return fmt.Sprintf("%s#%s", p, r)
|
|
}
|