Files
Marco de Abreu c47ab101d1 Dashboards: Add Dashboard Schema validation (2) (#103844)
* Activate schema validation and align underlying systems

* update to save as v0 if not the right schema version

* Resolve merge conflicts

* Move RequireApiErrorStatus to tests package

* Add mutation tests

* Fix lint

* Only do min version check if dashboard is v1

* Fix lint and disable provisioning test

* Revert provisioning changes

* Revert more tests and add schema test

* Reran gen

* SQL Dashboard save

* Adjust APIVERSION

* Fixed mutation test

* Add logging on downgrade

---------

Co-authored-by: Marco de Abreu <18629099+marcoabreu@users.noreply.github.com>
Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
2025-04-11 23:05:41 +02:00

113 lines
3.6 KiB
Go

package resources
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
"github.com/grafana/grafana-app-sdk/logging"
dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
)
var (
ErrUnableToReadResourceBytes = errors.New("unable to read bytes as a resource")
ErrUnableToReadPanelsMissing = errors.New("panels property is required")
ErrUnableToReadSchemaVersionMissing = errors.New("schemaVersion property is required")
ErrUnableToReadTagsMissing = errors.New("tags property is required")
ErrClassicResourceIsAlreadyK8sForm = errors.New("classic resource is already structured with apiVersion and kind")
)
// This reads a "classic" file format and will convert it to an unstructured k8s resource
// The file path may determine how the resource is parsed
//
// The context and logger are both only used for logging purposes. They do not control any logic.
func ReadClassicResource(ctx context.Context, info *repository.FileInfo) (*unstructured.Unstructured, *schema.GroupVersionKind, provisioning.ClassicFileType, error) {
var value map[string]any
// Try parsing as JSON
if info.Data[0] == '{' {
err := json.Unmarshal(info.Data, &value)
if err != nil {
return nil, nil, "", err
}
} else {
return nil, nil, "", fmt.Errorf("unable to read file")
}
// regular version headers exist
// TODO: do we intend on this checking Kind or kind? document reasoning.
if value["apiVersion"] != nil {
if value["kind"] != nil {
return nil, nil, "", ErrClassicResourceIsAlreadyK8sForm
}
logging.FromContext(ctx).Debug("TODO... likely a provisioning",
"apiVersion", value["apiVersion"],
"kind", value["Kind"])
gv, err := schema.ParseGroupVersion(value["apiVersion"].(string))
if err != nil {
return nil, nil, "", fmt.Errorf("invalid apiVersion")
}
gvk := gv.WithKind(value["Kind"].(string))
return &unstructured.Unstructured{Object: value}, &gvk, "", nil
}
// If this is a dashboard, convert it
if value["panels"] != nil &&
value["schemaVersion"] != nil &&
value["tags"] != nil {
gvk := &schema.GroupVersionKind{
Group: dashboard.GROUP,
Version: "v0alpha1", // no schema
Kind: "Dashboard"}
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": gvk.GroupVersion().String(),
"kind": gvk.Kind,
"metadata": map[string]any{
"name": value["uid"],
},
"spec": value,
},
}, gvk, provisioning.ClassicDashboard, nil
}
return nil, nil, "", ErrUnableToReadResourceBytes
}
// DecodeYAMLObject reads the input as YAML and outputs its Kubernetes resource, if it is one.
// Note that all JSON is also valid YAML, so this can also be used for JSON data.
func DecodeYAMLObject(input io.Reader) (*unstructured.Unstructured, *schema.GroupVersionKind, error) {
data, err := io.ReadAll(input)
if err != nil {
return nil, nil, err
}
obj, gvk, err := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme).
Decode(data, nil, nil)
if err != nil {
return nil, gvk, err
}
// The decoder should put it directly into an unstructured object
val, ok := obj.(*unstructured.Unstructured)
if ok {
return val, gvk, err
}
unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, gvk, err
}
return &unstructured.Unstructured{Object: unstructuredMap}, gvk, err
}