Dashboards API: v2alpha2 missing pieces (#108293)
* Dashboards API: v2alpha2 missing pieces * Fix issue with dashboard client scope for alpha versions As we now have 2 different alpha versions for v2 we need to store the clients separately. * Improve debuggability of provisioning export test - Add a helper function to print the tree structure. - Be explicit about the expected file names expected in each case. * Update pkg/registry/apis/dashboard/mutate.go * Update pkg/services/authz/zanzana/server/server.go Co-authored-by: Igor Suleymanov <radiohead@users.noreply.github.com> * Review * go lint --------- Co-authored-by: Roberto Jimenez Sanchez <roberto.jimenez@grafana.com> Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com> Co-authored-by: Igor Suleymanov <radiohead@users.noreply.github.com>
This commit is contained in:
co-authored by
Igor Suleymanov
Roberto Jimenez Sanchez
Stephanie Hingtgen
parent
eeb44c1d63
commit
d72a70d246
@@ -8,7 +8,8 @@ import (
|
||||
|
||||
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
dashboardV2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2"
|
||||
commonV0 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
@@ -35,8 +36,14 @@ func NewDashboardLargeObjectSupport(scheme *runtime.Scheme, threshold int) *apis
|
||||
reduceUnstructredSpec(&dash.Spec)
|
||||
case *dashboardV1.Dashboard:
|
||||
reduceUnstructredSpec(&dash.Spec)
|
||||
case *dashboardV2.Dashboard:
|
||||
dash.Spec = dashboardV2.DashboardSpec{
|
||||
case *dashboardV2alpha1.Dashboard:
|
||||
dash.Spec = dashboardV2alpha1.DashboardSpec{
|
||||
Title: dash.Spec.Title,
|
||||
Description: dash.Spec.Description,
|
||||
Tags: dash.Spec.Tags,
|
||||
}
|
||||
case *dashboardV2alpha2.Dashboard:
|
||||
dash.Spec = dashboardV2alpha2.DashboardSpec{
|
||||
Title: dash.Spec.Title,
|
||||
Description: dash.Spec.Description,
|
||||
Tags: dash.Spec.Tags,
|
||||
@@ -55,7 +62,9 @@ func NewDashboardLargeObjectSupport(scheme *runtime.Scheme, threshold int) *apis
|
||||
return dash.Spec.UnmarshalJSON(blob)
|
||||
case *dashboardV1.Dashboard:
|
||||
return dash.Spec.UnmarshalJSON(blob)
|
||||
case *dashboardV2.Dashboard:
|
||||
case *dashboardV2alpha1.Dashboard:
|
||||
return json.Unmarshal(blob, &dash.Spec)
|
||||
case *dashboardV2alpha2.Dashboard:
|
||||
return json.Unmarshal(blob, &dash.Spec)
|
||||
default:
|
||||
return fmt.Errorf("unsupported dashboard type %T", obj)
|
||||
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
dashv2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2"
|
||||
)
|
||||
|
||||
func TestLargeDashboardSupportV1(t *testing.T) {
|
||||
@@ -73,47 +74,47 @@ func TestLargeDashboardSupportV1(t *testing.T) {
|
||||
require.Len(t, panels, expectedPanelCount)
|
||||
}
|
||||
|
||||
func TestLargeDashboardSupportV2(t *testing.T) {
|
||||
func TestLargeDashboardSupportV2alpha1(t *testing.T) {
|
||||
// Test RebuildSpec functionality specifically for v2 dashboards
|
||||
// This tests the json.Unmarshal(blob, &dash.Spec) path for structured specs
|
||||
// unlike v0/v1 which use the UnmarshalJSON path for unstructured specs
|
||||
|
||||
// Create a v2 dashboard with structured spec
|
||||
originalV2Dash := &dashv2.Dashboard{
|
||||
originalV2Dash := &dashv2alpha1.Dashboard{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-v2",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: dashv2.DashboardSpec{
|
||||
Spec: dashv2alpha1.DashboardSpec{
|
||||
Title: "Test V2 Dashboard",
|
||||
Description: stringPtr("A test dashboard for v2 large object support"),
|
||||
Tags: []string{"test", "v2", "large-object"},
|
||||
Editable: boolPtr(true),
|
||||
LiveNow: boolPtr(false),
|
||||
Preload: false,
|
||||
Annotations: []dashv2.DashboardAnnotationQueryKind{
|
||||
Annotations: []dashv2alpha1.DashboardAnnotationQueryKind{
|
||||
{
|
||||
Kind: "AnnotationQuery",
|
||||
Spec: dashv2.DashboardAnnotationQuerySpec{
|
||||
Spec: dashv2alpha1.DashboardAnnotationQuerySpec{
|
||||
Name: "Test Annotation",
|
||||
},
|
||||
},
|
||||
},
|
||||
Elements: map[string]dashv2.DashboardElement{
|
||||
Elements: map[string]dashv2alpha1.DashboardElement{
|
||||
"panel-1": {
|
||||
PanelKind: &dashv2.DashboardPanelKind{},
|
||||
PanelKind: &dashv2alpha1.DashboardPanelKind{},
|
||||
},
|
||||
},
|
||||
Layout: dashv2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{},
|
||||
TimeSettings: dashv2.DashboardTimeSettingsSpec{},
|
||||
CursorSync: dashv2.DashboardDashboardCursorSyncOff,
|
||||
Variables: []dashv2.DashboardVariableKind{},
|
||||
Links: []dashv2.DashboardDashboardLink{},
|
||||
Layout: dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{},
|
||||
TimeSettings: dashv2alpha1.DashboardTimeSettingsSpec{},
|
||||
CursorSync: dashv2alpha1.DashboardDashboardCursorSyncOff,
|
||||
Variables: []dashv2alpha1.DashboardVariableKind{},
|
||||
Links: []dashv2alpha1.DashboardDashboardLink{},
|
||||
},
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
err := dashv2.AddToScheme(scheme)
|
||||
err := dashv2alpha1.AddToScheme(scheme)
|
||||
require.NoError(t, err)
|
||||
|
||||
largeObject := NewDashboardLargeObjectSupport(scheme, 0)
|
||||
@@ -142,7 +143,107 @@ func TestLargeDashboardSupportV2(t *testing.T) {
|
||||
require.Empty(t, dashToReduce.Spec.Links)
|
||||
|
||||
// Now test RebuildSpec - this is the key test for v2!
|
||||
rehydratedDash := &dashv2.Dashboard{
|
||||
rehydratedDash := &dashv2alpha1.Dashboard{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-v2-rehydrated",
|
||||
Namespace: "test",
|
||||
},
|
||||
}
|
||||
|
||||
// This tests the json.Unmarshal(blob, &dash.Spec) path for v2 dashboards
|
||||
err = largeObject.RebuildSpec(rehydratedDash, originalSpecBlob)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the full dashboard spec is restored correctly
|
||||
require.Equal(t, originalV2Dash.Spec.Title, rehydratedDash.Spec.Title)
|
||||
require.Equal(t, originalV2Dash.Spec.Description, rehydratedDash.Spec.Description)
|
||||
require.Equal(t, originalV2Dash.Spec.Tags, rehydratedDash.Spec.Tags)
|
||||
require.Equal(t, originalV2Dash.Spec.Editable, rehydratedDash.Spec.Editable)
|
||||
require.Equal(t, originalV2Dash.Spec.LiveNow, rehydratedDash.Spec.LiveNow)
|
||||
require.Equal(t, originalV2Dash.Spec.Preload, rehydratedDash.Spec.Preload)
|
||||
|
||||
// Verify annotations are restored
|
||||
require.Len(t, rehydratedDash.Spec.Annotations, 1)
|
||||
annotation := rehydratedDash.Spec.Annotations[0]
|
||||
require.Equal(t, "AnnotationQuery", annotation.Kind)
|
||||
require.Equal(t, "Test Annotation", annotation.Spec.Name)
|
||||
|
||||
// Verify elements are restored
|
||||
require.Len(t, rehydratedDash.Spec.Elements, 1)
|
||||
_, exists := rehydratedDash.Spec.Elements["panel-1"]
|
||||
require.True(t, exists)
|
||||
}
|
||||
|
||||
func TestLargeDashboardSupportV2alpha2(t *testing.T) {
|
||||
// Test RebuildSpec functionality specifically for v2 dashboards
|
||||
// This tests the json.Unmarshal(blob, &dash.Spec) path for structured specs
|
||||
// unlike v0/v1 which use the UnmarshalJSON path for unstructured specs
|
||||
|
||||
// Create a v2 dashboard with structured spec
|
||||
originalV2Dash := &dashv2alpha2.Dashboard{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-v2",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: dashv2alpha2.DashboardSpec{
|
||||
Title: "Test V2 Dashboard",
|
||||
Description: stringPtr("A test dashboard for v2 large object support"),
|
||||
Tags: []string{"test", "v2", "large-object"},
|
||||
Editable: boolPtr(true),
|
||||
LiveNow: boolPtr(false),
|
||||
Preload: false,
|
||||
Annotations: []dashv2alpha2.DashboardAnnotationQueryKind{
|
||||
{
|
||||
Kind: "AnnotationQuery",
|
||||
Spec: dashv2alpha2.DashboardAnnotationQuerySpec{
|
||||
Name: "Test Annotation",
|
||||
},
|
||||
},
|
||||
},
|
||||
Elements: map[string]dashv2alpha2.DashboardElement{
|
||||
"panel-1": {
|
||||
PanelKind: &dashv2alpha2.DashboardPanelKind{},
|
||||
},
|
||||
},
|
||||
Layout: dashv2alpha2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{},
|
||||
TimeSettings: dashv2alpha2.DashboardTimeSettingsSpec{},
|
||||
CursorSync: dashv2alpha2.DashboardDashboardCursorSyncOff,
|
||||
Variables: []dashv2alpha2.DashboardVariableKind{},
|
||||
Links: []dashv2alpha2.DashboardDashboardLink{},
|
||||
},
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
err := dashv2alpha2.AddToScheme(scheme)
|
||||
require.NoError(t, err)
|
||||
|
||||
largeObject := NewDashboardLargeObjectSupport(scheme, 0)
|
||||
|
||||
// Marshal the original spec to use as our "blob" data
|
||||
originalSpecBlob, err := json.Marshal(originalV2Dash.Spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a copy to test reduction
|
||||
dashToReduce := originalV2Dash.DeepCopy()
|
||||
|
||||
// Convert the dashboard to a small value (ReduceSpec)
|
||||
err = largeObject.ReduceSpec(dashToReduce)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify only essential fields remain after reduction
|
||||
require.Equal(t, "Test V2 Dashboard", dashToReduce.Spec.Title)
|
||||
require.Equal(t, stringPtr("A test dashboard for v2 large object support"), dashToReduce.Spec.Description)
|
||||
require.Equal(t, []string{"test", "v2", "large-object"}, dashToReduce.Spec.Tags)
|
||||
|
||||
// Everything else should be empty/default
|
||||
require.Empty(t, dashToReduce.Spec.Annotations)
|
||||
require.Empty(t, dashToReduce.Spec.Elements)
|
||||
require.Nil(t, dashToReduce.Spec.Layout.GridLayoutKind)
|
||||
require.Empty(t, dashToReduce.Spec.Variables)
|
||||
require.Empty(t, dashToReduce.Spec.Links)
|
||||
|
||||
// Now test RebuildSpec - this is the key test for v2!
|
||||
rehydratedDash := &dashv2alpha2.Dashboard{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-v2-rehydrated",
|
||||
Namespace: "test",
|
||||
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
|
||||
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
dashboardV2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
@@ -57,17 +58,30 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute
|
||||
Error: migrationErr.Error(),
|
||||
}
|
||||
}
|
||||
case *dashboardV2.Dashboard:
|
||||
|
||||
case *dashboardV2alpha1.Dashboard:
|
||||
// Temporary fix: The generator fails to properly initialize this property, so we'll do it here
|
||||
// until the generator is fixed.
|
||||
if v.Spec.Layout.GridLayoutKind == nil && v.Spec.Layout.RowsLayoutKind == nil && v.Spec.Layout.AutoGridLayoutKind == nil && v.Spec.Layout.TabsLayoutKind == nil {
|
||||
v.Spec.Layout.GridLayoutKind = &dashboardV2.DashboardGridLayoutKind{
|
||||
v.Spec.Layout.GridLayoutKind = &dashboardV2alpha1.DashboardGridLayoutKind{
|
||||
Kind: "GridLayout",
|
||||
Spec: dashboardV2.DashboardGridLayoutSpec{},
|
||||
Spec: dashboardV2alpha1.DashboardGridLayoutSpec{},
|
||||
}
|
||||
}
|
||||
|
||||
resourceInfo = dashboardV2.DashboardResourceInfo
|
||||
resourceInfo = dashboardV2alpha1.DashboardResourceInfo
|
||||
|
||||
case *dashboardV2alpha2.Dashboard:
|
||||
// Temporary fix: The generator fails to properly initialize this property, so we'll do it here
|
||||
// until the generator is fixed.
|
||||
if v.Spec.Layout.GridLayoutKind == nil && v.Spec.Layout.RowsLayoutKind == nil && v.Spec.Layout.AutoGridLayoutKind == nil && v.Spec.Layout.TabsLayoutKind == nil {
|
||||
v.Spec.Layout.GridLayoutKind = &dashboardV2alpha2.DashboardGridLayoutKind{
|
||||
Kind: "GridLayout",
|
||||
Spec: dashboardV2alpha2.DashboardGridLayoutSpec{},
|
||||
}
|
||||
}
|
||||
|
||||
resourceInfo = dashboardV2alpha2.DashboardResourceInfo
|
||||
|
||||
// Noop for V2
|
||||
default:
|
||||
|
||||
@@ -12,7 +12,8 @@ import (
|
||||
|
||||
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashv2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil"
|
||||
@@ -141,9 +142,19 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) {
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "v2 should set layout if it is not set",
|
||||
inputObj: &v2alpha1.Dashboard{
|
||||
Spec: v2alpha1.DashboardSpec{
|
||||
name: "v2alpha1 should set layout if it is not set",
|
||||
inputObj: &dashv2alpha1.Dashboard{
|
||||
Spec: dashv2alpha1.DashboardSpec{
|
||||
Title: "test123",
|
||||
},
|
||||
},
|
||||
operation: admission.Create,
|
||||
expectedTitle: "test123",
|
||||
},
|
||||
{
|
||||
name: "v2alpha2 should set layout if it is not set",
|
||||
inputObj: &dashv2alpha2.Dashboard{
|
||||
Spec: dashv2alpha2.DashboardSpec{
|
||||
Title: "test123",
|
||||
},
|
||||
},
|
||||
@@ -203,7 +214,8 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) {
|
||||
if tt.migrationExpected {
|
||||
require.Equal(t, schemaversion.LATEST_VERSION, schemaVersion, "dashboard should be migrated to the latest version")
|
||||
}
|
||||
case *v2alpha1.Dashboard:
|
||||
case *dashv2alpha1.Dashboard:
|
||||
case *dashv2alpha2.Dashboard:
|
||||
require.Equal(t, tt.expectedTitle, v.Spec.Title, "title should be set")
|
||||
require.NotNil(t, v.Spec.Layout, "layout should be set")
|
||||
require.NotNil(t, v.Spec.Layout.GridLayoutKind, "layout should be a GridLayout")
|
||||
|
||||
@@ -12,7 +12,8 @@ import (
|
||||
|
||||
v0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
v1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
v2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
v2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
v2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
@@ -32,7 +33,8 @@ func (b *DashboardsAPIBuilder) ValidateDashboardSpec(ctx context.Context, obj ru
|
||||
errorOnSchemaMismatches = false // Never error for v0
|
||||
case *v1.Dashboard:
|
||||
errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV1)
|
||||
case *v2.Dashboard:
|
||||
case *v2alpha1.Dashboard:
|
||||
case *v2alpha2.Dashboard:
|
||||
errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV2)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid dashboard type: %T", obj)
|
||||
@@ -52,8 +54,10 @@ func (b *DashboardsAPIBuilder) ValidateDashboardSpec(ctx context.Context, obj ru
|
||||
errors, schemaVersionError = v0.ValidateDashboardSpec(v, alwaysLogSchemaValidationErrors)
|
||||
case *v1.Dashboard:
|
||||
errors, schemaVersionError = v1.ValidateDashboardSpec(v, alwaysLogSchemaValidationErrors)
|
||||
case *v2.Dashboard:
|
||||
errors = v2.ValidateDashboardSpec(v)
|
||||
case *v2alpha1.Dashboard:
|
||||
errors = v2alpha1.ValidateDashboardSpec(v)
|
||||
case *v2alpha2.Dashboard:
|
||||
errors = v2alpha2.ValidateDashboardSpec(v)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,21 +38,30 @@ func ExportResources(ctx context.Context, options provisioning.ExportJobOptions,
|
||||
// When requesting v2 (or v0) dashboards over the v1 api, we want to keep the original apiVersion if conversion fails
|
||||
var shim conversionShim
|
||||
if kind.GroupResource() == resources.DashboardResource.GroupResource() {
|
||||
var v2client dynamic.ResourceInterface
|
||||
var v2clientAlphaV1, v2clientAlphaV2 dynamic.ResourceInterface
|
||||
shim = func(ctx context.Context, item *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||
failed, _, _ := unstructured.NestedBool(item.Object, "status", "conversion", "failed")
|
||||
if failed {
|
||||
storedVersion, _, _ := unstructured.NestedString(item.Object, "status", "conversion", "storedVersion")
|
||||
|
||||
// For v2 we need to request the original version
|
||||
if strings.HasPrefix(storedVersion, "v2") {
|
||||
if v2client == nil {
|
||||
v2client, _, err = clients.ForResource(resources.DashboardResourceV2)
|
||||
if strings.HasPrefix(storedVersion, "v2alpha1") {
|
||||
if v2clientAlphaV1 == nil {
|
||||
v2clientAlphaV1, _, err = clients.ForResource(resources.DashboardResourceV2alpha1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return v2client.Get(ctx, item.GetName(), metav1.GetOptions{})
|
||||
return v2clientAlphaV1.Get(ctx, item.GetName(), metav1.GetOptions{})
|
||||
}
|
||||
|
||||
if strings.HasPrefix(storedVersion, "v2alpha2") {
|
||||
if v2clientAlphaV2 == nil {
|
||||
v2clientAlphaV2, _, err = clients.ForResource(resources.DashboardResourceV2alpha2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return v2clientAlphaV2.Get(ctx, item.GetName(), metav1.GetOptions{})
|
||||
}
|
||||
|
||||
// For v0 we can simply fallback -- the full model is saved, but
|
||||
@@ -97,6 +106,7 @@ func exportResource(ctx context.Context,
|
||||
if shim != nil {
|
||||
item, err = shim(ctx, item)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
result.Path, err = repositoryResources.WriteResourceFileFromObject(ctx, item, resources.WriteOptions{
|
||||
Path: options.Path,
|
||||
|
||||
@@ -16,432 +16,519 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
||||
)
|
||||
|
||||
func TestExportResources_Dashboards(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mockItems []unstructured.Unstructured
|
||||
expectedError string
|
||||
setupProgress func(progress *jobs.MockJobProgressRecorder)
|
||||
setupResources func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind)
|
||||
}{
|
||||
{
|
||||
name: "successful dashboard export",
|
||||
mockItems: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "dashboard-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "dashboard-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
setupProgress: func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "dashboard-1" && result.Action == repository.FileActionCreated
|
||||
})).Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "dashboard-2" && result.Action == repository.FileActionCreated
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
},
|
||||
setupResources: func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
options := resources.WriteOptions{
|
||||
Path: "grafana",
|
||||
Ref: "feature/branch",
|
||||
}
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetName() == "dashboard-1"
|
||||
}), options).Return("dashboard-1.json", nil)
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetName() == "dashboard-2"
|
||||
}), options).Return("dashboard-2.json", nil)
|
||||
// Helper function to create dashboard objects
|
||||
func createDashboardObject(name string) unstructured.Unstructured {
|
||||
return unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": name,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client error",
|
||||
mockItems: nil,
|
||||
expectedError: "get client for dashboards: didn't work",
|
||||
setupProgress: func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
},
|
||||
setupResources: func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, fmt.Errorf("didn't work"))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard export with errors",
|
||||
mockItems: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "dashboard-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "dashboard-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
setupProgress: func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "dashboard-1" && result.Action == repository.FileActionIgnored && result.Error != nil && result.Error.Error() == "failed to export dashboard"
|
||||
})).Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "dashboard-2" && result.Action == repository.FileActionCreated
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
},
|
||||
setupResources: func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
options := resources.WriteOptions{
|
||||
Path: "grafana",
|
||||
Ref: "feature/branch",
|
||||
}
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetName() == "dashboard-1"
|
||||
}), options).Return("", fmt.Errorf("failed to export dashboard"))
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetName() == "dashboard-2"
|
||||
}), options).Return("dashboard-2.json", nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard export too many errors",
|
||||
mockItems: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "dashboard-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "export dashboards: too many errors encountered",
|
||||
setupProgress: func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "dashboard-1" && result.Action == repository.FileActionIgnored && result.Error != nil && result.Error.Error() == "failed to export dashboard"
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(fmt.Errorf("too many errors encountered"))
|
||||
},
|
||||
setupResources: func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
options := resources.WriteOptions{
|
||||
Path: "grafana",
|
||||
Ref: "feature/branch",
|
||||
}
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetName() == "dashboard-1"
|
||||
}), options).Return("", fmt.Errorf("failed to export dashboard"))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignores existing dashboards",
|
||||
mockItems: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "existing-dashboard",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
setupProgress: func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "existing-dashboard" && result.Action == repository.FileActionIgnored
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
},
|
||||
setupResources: func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
options := resources.WriteOptions{
|
||||
Path: "grafana",
|
||||
Ref: "feature/branch",
|
||||
}
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetName() == "existing-dashboard"
|
||||
}), options).Return("", resources.ErrAlreadyInRepository)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uses saved dashboard version",
|
||||
mockItems: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "existing-dashboard",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"hello": "world",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conversion": map[string]interface{}{
|
||||
"failed": true,
|
||||
"storedVersion": "v0xyz",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
setupProgress: func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "existing-dashboard" && result.Action == repository.FileActionIgnored
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
},
|
||||
setupResources: func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
options := resources.WriteOptions{
|
||||
Path: "grafana",
|
||||
Ref: "feature/branch",
|
||||
}
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
// Verify that the object has the expected status.conversion.storedVersion field
|
||||
status, exists, err := unstructured.NestedMap(obj.Object, "status")
|
||||
if !exists || err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
conversion, exists, err := unstructured.NestedMap(status, "conversion")
|
||||
if !exists || err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
storedVersion, exists, err := unstructured.NestedString(conversion, "storedVersion")
|
||||
if !exists || err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if storedVersion != "v0xyz" {
|
||||
return false
|
||||
}
|
||||
|
||||
return obj.GetName() == "existing-dashboard"
|
||||
}), options).Return("", fmt.Errorf("XXX"))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard with failed conversion but no stored version",
|
||||
mockItems: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "dashboard-no-stored-version",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conversion": map[string]interface{}{
|
||||
"failed": true,
|
||||
// No storedVersion field
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
setupProgress: func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "dashboard-no-stored-version" &&
|
||||
result.Action == repository.FileActionIgnored &&
|
||||
result.Error != nil
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
},
|
||||
setupResources: func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
// The value is not saved
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "handles v2 dashboard version",
|
||||
mockItems: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "v2-dashboard",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conversion": map[string]interface{}{
|
||||
"failed": true,
|
||||
"storedVersion": "v2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
setupProgress: func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "v2-dashboard" && result.Action == repository.FileActionCreated
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
},
|
||||
setupResources: func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
// Setup v1 client
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
|
||||
// Setup v2 client
|
||||
|
||||
// Mock v2 client Get call
|
||||
v2Dashboard := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "dashboard.grafana.app/v2alpha1",
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "v2-dashboard",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"version": 2,
|
||||
"title": "V2 Dashboard",
|
||||
},
|
||||
},
|
||||
}
|
||||
v2Client := &mockDynamicInterface{items: []unstructured.Unstructured{*v2Dashboard}}
|
||||
resourceClients.On("ForResource", resources.DashboardResourceV2).Return(v2Client, gvk, nil)
|
||||
|
||||
options := resources.WriteOptions{
|
||||
Path: "grafana",
|
||||
Ref: "feature/branch",
|
||||
}
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, v2Dashboard, options).Return("v2-dashboard.json", nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "handles v2 client creation error",
|
||||
mockItems: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "v2-dashboard-error",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conversion": map[string]interface{}{
|
||||
"failed": true,
|
||||
"storedVersion": "v2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
setupProgress: func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
if result.Name != "v2-dashboard-error" {
|
||||
return false
|
||||
}
|
||||
if result.Action != repository.FileActionIgnored {
|
||||
return false
|
||||
}
|
||||
if result.Error == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if result.Error.Error() != "v2 client error" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
},
|
||||
setupResources: func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
resourceClients.On("ForResource", resources.DashboardResourceV2).Return(nil, gvk, fmt.Errorf("v2 client error"))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockClient := &mockDynamicInterface{
|
||||
items: tt.mockItems,
|
||||
}
|
||||
|
||||
resourceClients := resources.NewMockResourceClients(t)
|
||||
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
||||
tt.setupProgress(mockProgress)
|
||||
|
||||
repoResources := resources.NewMockRepositoryResources(t)
|
||||
tt.setupResources(repoResources, resourceClients, mockClient, schema.GroupVersionKind{
|
||||
Group: resources.DashboardResource.Group,
|
||||
Version: resources.DashboardResource.Version,
|
||||
Kind: "DashboardList",
|
||||
})
|
||||
|
||||
options := provisioningV0.ExportJobOptions{
|
||||
Path: "grafana",
|
||||
Branch: "feature/branch",
|
||||
}
|
||||
|
||||
err := ExportResources(context.Background(), options, resourceClients, repoResources, mockProgress)
|
||||
if tt.expectedError != "" {
|
||||
require.EqualError(t, err, tt.expectedError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
mockProgress.AssertExpectations(t)
|
||||
repoResources.AssertExpectations(t)
|
||||
resourceClients.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create v2 dashboard objects
|
||||
func createV2DashboardObject(name, version string) unstructured.Unstructured {
|
||||
return unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": fmt.Sprintf("dashboard.grafana.app/%s", version),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": name,
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"version": 2,
|
||||
"title": "V2 Dashboard",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to run export test
|
||||
func runExportTest(t *testing.T, mockItems []unstructured.Unstructured, setupProgress func(*jobs.MockJobProgressRecorder), setupResources func(*resources.MockRepositoryResources, *resources.MockResourceClients, *mockDynamicInterface, schema.GroupVersionKind)) error {
|
||||
mockClient := &mockDynamicInterface{
|
||||
items: mockItems,
|
||||
}
|
||||
|
||||
resourceClients := resources.NewMockResourceClients(t)
|
||||
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
||||
setupProgress(mockProgress)
|
||||
|
||||
repoResources := resources.NewMockRepositoryResources(t)
|
||||
setupResources(repoResources, resourceClients, mockClient, schema.GroupVersionKind{
|
||||
Group: resources.DashboardResource.Group,
|
||||
Version: resources.DashboardResource.Version,
|
||||
Kind: "DashboardList",
|
||||
})
|
||||
|
||||
options := provisioningV0.ExportJobOptions{
|
||||
Path: "grafana",
|
||||
Branch: "feature/branch",
|
||||
}
|
||||
|
||||
err := ExportResources(context.Background(), options, resourceClients, repoResources, mockProgress)
|
||||
|
||||
mockProgress.AssertExpectations(t)
|
||||
repoResources.AssertExpectations(t)
|
||||
resourceClients.AssertExpectations(t)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func TestExportResources_Dashboards_Success(t *testing.T) {
|
||||
mockItems := []unstructured.Unstructured{
|
||||
createDashboardObject("dashboard-1"),
|
||||
createDashboardObject("dashboard-2"),
|
||||
}
|
||||
|
||||
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "dashboard-1" && result.Action == repository.FileActionCreated
|
||||
})).Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "dashboard-2" && result.Action == repository.FileActionCreated
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
}
|
||||
|
||||
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
options := resources.WriteOptions{
|
||||
Path: "grafana",
|
||||
Ref: "feature/branch",
|
||||
}
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetName() == "dashboard-1"
|
||||
}), options).Return("dashboard-1.json", nil)
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetName() == "dashboard-2"
|
||||
}), options).Return("dashboard-2.json", nil)
|
||||
}
|
||||
|
||||
err := runExportTest(t, mockItems, setupProgress, setupResources)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestExportResources_Dashboards_ClientError(t *testing.T) {
|
||||
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
}
|
||||
|
||||
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, fmt.Errorf("didn't work"))
|
||||
}
|
||||
|
||||
err := runExportTest(t, nil, setupProgress, setupResources)
|
||||
require.EqualError(t, err, "get client for dashboards: didn't work")
|
||||
}
|
||||
|
||||
func TestExportResources_Dashboards_WithErrors(t *testing.T) {
|
||||
mockItems := []unstructured.Unstructured{
|
||||
createDashboardObject("dashboard-1"),
|
||||
createDashboardObject("dashboard-2"),
|
||||
}
|
||||
|
||||
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "dashboard-1" && result.Action == repository.FileActionIgnored && result.Error != nil && result.Error.Error() == "failed to export dashboard"
|
||||
})).Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "dashboard-2" && result.Action == repository.FileActionCreated
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
}
|
||||
|
||||
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
options := resources.WriteOptions{
|
||||
Path: "grafana",
|
||||
Ref: "feature/branch",
|
||||
}
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetName() == "dashboard-1"
|
||||
}), options).Return("", fmt.Errorf("failed to export dashboard"))
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetName() == "dashboard-2"
|
||||
}), options).Return("dashboard-2.json", nil)
|
||||
}
|
||||
|
||||
err := runExportTest(t, mockItems, setupProgress, setupResources)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestExportResources_Dashboards_TooManyErrors(t *testing.T) {
|
||||
mockItems := []unstructured.Unstructured{
|
||||
createDashboardObject("dashboard-1"),
|
||||
}
|
||||
|
||||
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "dashboard-1" && result.Action == repository.FileActionIgnored && result.Error != nil && result.Error.Error() == "failed to export dashboard"
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(fmt.Errorf("too many errors encountered"))
|
||||
}
|
||||
|
||||
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
options := resources.WriteOptions{
|
||||
Path: "grafana",
|
||||
Ref: "feature/branch",
|
||||
}
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetName() == "dashboard-1"
|
||||
}), options).Return("", fmt.Errorf("failed to export dashboard"))
|
||||
}
|
||||
|
||||
err := runExportTest(t, mockItems, setupProgress, setupResources)
|
||||
require.EqualError(t, err, "export dashboards: too many errors encountered")
|
||||
}
|
||||
|
||||
func TestExportResources_Dashboards_IgnoresExisting(t *testing.T) {
|
||||
mockItems := []unstructured.Unstructured{
|
||||
createDashboardObject("existing-dashboard"),
|
||||
}
|
||||
|
||||
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "existing-dashboard" && result.Action == repository.FileActionIgnored
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
}
|
||||
|
||||
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
options := resources.WriteOptions{
|
||||
Path: "grafana",
|
||||
Ref: "feature/branch",
|
||||
}
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetName() == "existing-dashboard"
|
||||
}), options).Return("", resources.ErrAlreadyInRepository)
|
||||
}
|
||||
|
||||
err := runExportTest(t, mockItems, setupProgress, setupResources)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestExportResources_Dashboards_SavedVersion(t *testing.T) {
|
||||
mockItems := []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "existing-dashboard",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"hello": "world",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conversion": map[string]interface{}{
|
||||
"failed": true,
|
||||
"storedVersion": "v0xyz",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "existing-dashboard" && result.Action == repository.FileActionIgnored
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
}
|
||||
|
||||
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
options := resources.WriteOptions{
|
||||
Path: "grafana",
|
||||
Ref: "feature/branch",
|
||||
}
|
||||
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, mock.MatchedBy(func(obj *unstructured.Unstructured) bool {
|
||||
// Verify that the object has the expected status.conversion.storedVersion field
|
||||
status, exists, err := unstructured.NestedMap(obj.Object, "status")
|
||||
if !exists || err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
conversion, exists, err := unstructured.NestedMap(status, "conversion")
|
||||
if !exists || err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
storedVersion, exists, err := unstructured.NestedString(conversion, "storedVersion")
|
||||
if !exists || err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if storedVersion != "v0xyz" {
|
||||
return false
|
||||
}
|
||||
|
||||
return obj.GetName() == "existing-dashboard"
|
||||
}), options).Return("", fmt.Errorf("XXX"))
|
||||
}
|
||||
|
||||
err := runExportTest(t, mockItems, setupProgress, setupResources)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestExportResources_Dashboards_FailedConversionNoStoredVersion(t *testing.T) {
|
||||
mockItems := []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "dashboard-no-stored-version",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conversion": map[string]interface{}{
|
||||
"failed": true,
|
||||
// No storedVersion field
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "dashboard-no-stored-version" &&
|
||||
result.Action == repository.FileActionIgnored &&
|
||||
result.Error != nil
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
}
|
||||
|
||||
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
// The value is not saved
|
||||
}
|
||||
|
||||
err := runExportTest(t, mockItems, setupProgress, setupResources)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestExportResources_Dashboards_V2Alpha1(t *testing.T) {
|
||||
mockItems := []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "v2-dashboard",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conversion": map[string]interface{}{
|
||||
"failed": true,
|
||||
"storedVersion": "v2alpha1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "v2-dashboard" && result.Action == repository.FileActionCreated
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
}
|
||||
|
||||
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
// Setup v1 client
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
|
||||
// Setup v2 client
|
||||
v2Dashboard := createV2DashboardObject("v2-dashboard", "v2alpha1")
|
||||
v2Client := &mockDynamicInterface{items: []unstructured.Unstructured{v2Dashboard}}
|
||||
resourceClients.On("ForResource", resources.DashboardResourceV2alpha1).Return(v2Client, gvk, nil)
|
||||
|
||||
options := resources.WriteOptions{
|
||||
Path: "grafana",
|
||||
Ref: "feature/branch",
|
||||
}
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, &v2Dashboard, options).Return("v2-dashboard.json", nil)
|
||||
}
|
||||
|
||||
err := runExportTest(t, mockItems, setupProgress, setupResources)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestExportResources_Dashboards_V2Alpha1_ClientError(t *testing.T) {
|
||||
mockItems := []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "v2-dashboard-error",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conversion": map[string]interface{}{
|
||||
"failed": true,
|
||||
"storedVersion": "v2alpha1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
if result.Name != "v2-dashboard-error" {
|
||||
return false
|
||||
}
|
||||
if result.Action != repository.FileActionIgnored {
|
||||
return false
|
||||
}
|
||||
if result.Error == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if result.Error.Error() != "v2 client error" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
}
|
||||
|
||||
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResourceV2alpha1).Return(nil, gvk, fmt.Errorf("v2 client error"))
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
}
|
||||
|
||||
err := runExportTest(t, mockItems, setupProgress, setupResources)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestExportResources_Dashboards_V2Alpha2(t *testing.T) {
|
||||
mockItems := []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "v2-dashboard",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conversion": map[string]interface{}{
|
||||
"failed": true,
|
||||
"storedVersion": "v2alpha2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
return result.Name == "v2-dashboard" && result.Action == repository.FileActionCreated
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
}
|
||||
|
||||
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
// Setup v1 client
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
|
||||
// Setup v2 client
|
||||
v2Dashboard := createV2DashboardObject("v2-dashboard", "v2alpha2")
|
||||
v2Client := &mockDynamicInterface{items: []unstructured.Unstructured{v2Dashboard}}
|
||||
resourceClients.On("ForResource", resources.DashboardResourceV2alpha2).Return(v2Client, gvk, nil)
|
||||
|
||||
options := resources.WriteOptions{
|
||||
Path: "grafana",
|
||||
Ref: "feature/branch",
|
||||
}
|
||||
repoResources.On("WriteResourceFileFromObject", mock.Anything, &v2Dashboard, options).Return("v2-dashboard.json", nil)
|
||||
}
|
||||
|
||||
err := runExportTest(t, mockItems, setupProgress, setupResources)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestExportResources_Dashboards_V2Alpha2_ClientError(t *testing.T) {
|
||||
mockItems := []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": resources.DashboardResource.GroupVersion().String(),
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "v2-dashboard-error",
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conversion": map[string]interface{}{
|
||||
"failed": true,
|
||||
"storedVersion": "v2alpha2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
setupProgress := func(progress *jobs.MockJobProgressRecorder) {
|
||||
progress.On("SetMessage", mock.Anything, "start resource export").Return()
|
||||
progress.On("SetMessage", mock.Anything, "export dashboards").Return()
|
||||
progress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool {
|
||||
if result.Name != "v2-dashboard-error" {
|
||||
return false
|
||||
}
|
||||
if result.Action != repository.FileActionIgnored {
|
||||
return false
|
||||
}
|
||||
if result.Error == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if result.Error.Error() != "v2 client error" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})).Return()
|
||||
progress.On("TooManyErrors").Return(nil)
|
||||
}
|
||||
|
||||
setupResources := func(repoResources *resources.MockRepositoryResources, resourceClients *resources.MockResourceClients, mockClient *mockDynamicInterface, gvk schema.GroupVersionKind) {
|
||||
resourceClients.On("ForResource", resources.DashboardResourceV2alpha2).Return(nil, gvk, fmt.Errorf("v2 client error"))
|
||||
resourceClients.On("ForResource", resources.DashboardResource).Return(mockClient, gvk, nil)
|
||||
}
|
||||
|
||||
err := runExportTest(t, mockItems, setupProgress, setupResources)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
dashboardV2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2"
|
||||
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||
iam "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
@@ -19,10 +20,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
UserResource = iam.UserResourceInfo.GroupVersionResource()
|
||||
FolderResource = folders.FolderResourceInfo.GroupVersionResource()
|
||||
DashboardResource = dashboardV1.DashboardResourceInfo.GroupVersionResource()
|
||||
DashboardResourceV2 = dashboardV2.DashboardResourceInfo.GroupVersionResource()
|
||||
UserResource = iam.UserResourceInfo.GroupVersionResource()
|
||||
FolderResource = folders.FolderResourceInfo.GroupVersionResource()
|
||||
DashboardResource = dashboardV1.DashboardResourceInfo.GroupVersionResource()
|
||||
DashboardResourceV2alpha1 = dashboardV2alpha1.DashboardResourceInfo.GroupVersionResource()
|
||||
DashboardResourceV2alpha2 = dashboardV2alpha2.DashboardResourceInfo.GroupVersionResource()
|
||||
|
||||
// SupportedProvisioningResources is the list of resources that can fully managed from the UI
|
||||
SupportedProvisioningResources = []schema.GroupVersionResource{FolderResource, DashboardResource}
|
||||
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
|
||||
dashboardalpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2"
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
@@ -87,8 +88,21 @@ func (s *Server) getContextuals(subject string) (*openfgav1.ContextualTupleKeys,
|
||||
User: subject,
|
||||
Relation: common.RelationSetView,
|
||||
Object: common.NewGroupResourceIdent(
|
||||
dashboardalpha1.DashboardResourceInfo.GroupResource().Group,
|
||||
dashboardalpha1.DashboardResourceInfo.GroupResource().Resource,
|
||||
dashboardV2alpha1.DashboardResourceInfo.GroupResource().Group,
|
||||
dashboardV2alpha1.DashboardResourceInfo.GroupResource().Resource,
|
||||
"",
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
contextuals = append(
|
||||
contextuals,
|
||||
&openfgav1.TupleKey{
|
||||
User: subject,
|
||||
Relation: common.RelationSetView,
|
||||
Object: common.NewGroupResourceIdent(
|
||||
dashboardV2alpha2.DashboardResourceInfo.GroupResource().Group,
|
||||
dashboardV2alpha2.DashboardResourceInfo.GroupResource().Resource,
|
||||
"",
|
||||
),
|
||||
},
|
||||
|
||||
@@ -23,7 +23,8 @@ import (
|
||||
|
||||
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
dashboardV2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -164,10 +165,10 @@ func TestIntegrationDashboardsAppV1(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationDashboardsAppV2(t *testing.T) {
|
||||
func TestIntegrationDashboardsAppV2alpha1(t *testing.T) {
|
||||
gvr := schema.GroupVersionResource{
|
||||
Group: dashboardV2.GROUP,
|
||||
Version: dashboardV2.VERSION,
|
||||
Group: dashboardV2alpha1.GROUP,
|
||||
Version: dashboardV2alpha1.VERSION,
|
||||
Resource: "dashboards",
|
||||
}
|
||||
if testing.Short() {
|
||||
@@ -176,7 +177,33 @@ func TestIntegrationDashboardsAppV2(t *testing.T) {
|
||||
|
||||
modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5}
|
||||
for _, mode := range modes {
|
||||
t.Run(fmt.Sprintf("v1beta1 with dual writer mode %d", mode), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("v2alpha1 with dual writer mode %d", mode), func(t *testing.T) {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableAnonymous: true,
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"dashboards.dashboard.grafana.app": {
|
||||
DualWriterMode: mode,
|
||||
},
|
||||
},
|
||||
})
|
||||
runDashboardTest(t, helper, gvr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationDashboardsAppV2alpha2(t *testing.T) {
|
||||
gvr := schema.GroupVersionResource{
|
||||
Group: dashboardV2alpha2.GROUP,
|
||||
Version: dashboardV2alpha2.VERSION,
|
||||
Resource: "dashboards",
|
||||
}
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5}
|
||||
for _, mode := range modes {
|
||||
t.Run(fmt.Sprintf("v1alpha2 with dual writer mode %d", mode), func(t *testing.T) {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableAnonymous: true,
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
@@ -223,7 +250,7 @@ func TestIntegrationLegacySupport(t *testing.T) {
|
||||
|
||||
clientV2 := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: helper.Org1.Admin,
|
||||
GVR: dashboardV2.DashboardResourceInfo.GroupVersionResource(),
|
||||
GVR: dashboardV2alpha1.DashboardResourceInfo.GroupVersionResource(),
|
||||
})
|
||||
obj, err = clientV2.Resource.Create(ctx,
|
||||
helper.LoadYAMLOrJSONFile("testdata/dashboard-test-v2.yaml"),
|
||||
|
||||
@@ -16,7 +16,8 @@ import (
|
||||
|
||||
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
dashboardV2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2"
|
||||
foldersV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -345,11 +346,29 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
|
||||
},
|
||||
{
|
||||
name: "v2alpha1 dashboard with correct spec should not throw on v2",
|
||||
resourceInfo: dashboardV2.DashboardResourceInfo,
|
||||
resourceInfo: dashboardV2alpha1.DashboardResourceInfo,
|
||||
expectSpecErr: false,
|
||||
testObject: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": dashboardV2.DashboardResourceInfo.TypeMeta().APIVersion,
|
||||
"apiVersion": dashboardV2alpha1.DashboardResourceInfo.TypeMeta().APIVersion,
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"generateName": "test-",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"title": "Dashboard Title",
|
||||
"description": "valid description",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v2alpha2 dashboard with correct spec should not throw on v2",
|
||||
resourceInfo: dashboardV2alpha2.DashboardResourceInfo,
|
||||
expectSpecErr: false,
|
||||
testObject: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": dashboardV2alpha2.DashboardResourceInfo.TypeMeta().APIVersion,
|
||||
"kind": "Dashboard",
|
||||
"metadata": map[string]interface{}{
|
||||
"generateName": "test-",
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
apiVersion: dashboard.grafana.app/v2alpha1
|
||||
kind: Dashboard
|
||||
metadata:
|
||||
name: test-v2
|
||||
name: test-v2alpha1
|
||||
spec:
|
||||
title: Test dashboard. Created at v2
|
||||
title: Test dashboard. Created at v2alpha1
|
||||
layout:
|
||||
kind: GridLayout
|
||||
spec:
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: dashboard.grafana.app/v2alpha2
|
||||
kind: Dashboard
|
||||
metadata:
|
||||
name: test-v2alpha2
|
||||
spec:
|
||||
title: Test dashboard. Created at v2alpha2
|
||||
layout:
|
||||
kind: GridLayout
|
||||
spec:
|
||||
items: []
|
||||
@@ -21,7 +21,8 @@ import (
|
||||
|
||||
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
dashboardV2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardsV2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
||||
dashboardsV2alpha2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha2"
|
||||
folder "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
@@ -41,15 +42,16 @@ type provisioningTestHelper struct {
|
||||
*apis.K8sTestHelper
|
||||
ProvisioningPath string
|
||||
|
||||
Repositories *apis.K8sResourceClient
|
||||
Jobs *apis.K8sResourceClient
|
||||
Folders *apis.K8sResourceClient
|
||||
DashboardsV0 *apis.K8sResourceClient
|
||||
DashboardsV1 *apis.K8sResourceClient
|
||||
DashboardsV2 *apis.K8sResourceClient
|
||||
AdminREST *rest.RESTClient
|
||||
EditorREST *rest.RESTClient
|
||||
ViewerREST *rest.RESTClient
|
||||
Repositories *apis.K8sResourceClient
|
||||
Jobs *apis.K8sResourceClient
|
||||
Folders *apis.K8sResourceClient
|
||||
DashboardsV0 *apis.K8sResourceClient
|
||||
DashboardsV1 *apis.K8sResourceClient
|
||||
DashboardsV2alpha1 *apis.K8sResourceClient
|
||||
DashboardsV2alpha2 *apis.K8sResourceClient
|
||||
AdminREST *rest.RESTClient
|
||||
EditorREST *rest.RESTClient
|
||||
ViewerREST *rest.RESTClient
|
||||
}
|
||||
|
||||
func (h *provisioningTestHelper) SyncAndWait(t *testing.T, repo string, options *provisioning.SyncJobOptions) {
|
||||
@@ -255,10 +257,15 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper
|
||||
Namespace: "default", // actually org1
|
||||
GVR: dashboardV1.DashboardResourceInfo.GroupVersionResource(),
|
||||
})
|
||||
dashboardsV2 := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
dashboardsV2alpha1 := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: helper.Org1.Admin,
|
||||
Namespace: "default", // actually org1
|
||||
GVR: dashboardV2.DashboardResourceInfo.GroupVersionResource(),
|
||||
GVR: dashboardsV2alpha1.DashboardResourceInfo.GroupVersionResource(),
|
||||
})
|
||||
dashboardsV2alpha2 := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: helper.Org1.Admin,
|
||||
Namespace: "default", // actually org1
|
||||
GVR: dashboardsV2alpha2.DashboardResourceInfo.GroupVersionResource(),
|
||||
})
|
||||
|
||||
// Repo client, but less guard rails. Useful for subresources. We'll need this later...
|
||||
@@ -289,15 +296,16 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper
|
||||
ProvisioningPath: provisioningPath,
|
||||
K8sTestHelper: helper,
|
||||
|
||||
Repositories: repositories,
|
||||
AdminREST: adminClient,
|
||||
EditorREST: editorClient,
|
||||
ViewerREST: viewerClient,
|
||||
Jobs: jobs,
|
||||
Folders: folders,
|
||||
DashboardsV0: dashboardsV0,
|
||||
DashboardsV1: dashboardsV1,
|
||||
DashboardsV2: dashboardsV2,
|
||||
Repositories: repositories,
|
||||
AdminREST: adminClient,
|
||||
EditorREST: editorClient,
|
||||
ViewerREST: viewerClient,
|
||||
Jobs: jobs,
|
||||
Folders: folders,
|
||||
DashboardsV0: dashboardsV0,
|
||||
DashboardsV1: dashboardsV1,
|
||||
DashboardsV2alpha1: dashboardsV2alpha1,
|
||||
DashboardsV2alpha2: dashboardsV2alpha2,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -21,11 +22,50 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/extensions"
|
||||
"github.com/grafana/grafana/pkg/infra/slugify"
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/tests/apis"
|
||||
)
|
||||
|
||||
// printFileTree prints the directory structure as a tree for debugging purposes
|
||||
func printFileTree(t *testing.T, rootPath string) {
|
||||
t.Helper()
|
||||
t.Logf("File tree for %s:", rootPath)
|
||||
|
||||
err := filepath.WalkDir(rootPath, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(rootPath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if relPath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
depth := strings.Count(relPath, string(filepath.Separator))
|
||||
indent := strings.Repeat(" ", depth)
|
||||
|
||||
if d.IsDir() {
|
||||
t.Logf("%s├── %s/", indent, d.Name())
|
||||
} else {
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
t.Logf("%s├── %s (error reading info)", indent, d.Name())
|
||||
} else {
|
||||
t.Logf("%s├── %s (%d bytes)", indent, d.Name(), info.Size())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Logf("Error walking directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
@@ -609,9 +649,13 @@ func TestProvisioning_ExportUnifiedToRepository(t *testing.T) {
|
||||
_, err = helper.DashboardsV1.Resource.Create(ctx, dashboard, metav1.CreateOptions{})
|
||||
require.NoError(t, err, "should be able to create v1 dashboard")
|
||||
|
||||
dashboard = helper.LoadYAMLOrJSONFile("exportunifiedtorepository/dashboard-test-v2.yaml")
|
||||
_, err = helper.DashboardsV2.Resource.Create(ctx, dashboard, metav1.CreateOptions{})
|
||||
require.NoError(t, err, "should be able to create v2 dashboard")
|
||||
dashboard = helper.LoadYAMLOrJSONFile("exportunifiedtorepository/dashboard-test-v2alpha1.yaml")
|
||||
_, err = helper.DashboardsV2alpha1.Resource.Create(ctx, dashboard, metav1.CreateOptions{})
|
||||
require.NoError(t, err, "should be able to create v2alpha1 dashboard")
|
||||
|
||||
dashboard = helper.LoadYAMLOrJSONFile("exportunifiedtorepository/dashboard-test-v2alpha2.yaml")
|
||||
_, err = helper.DashboardsV2alpha2.Resource.Create(ctx, dashboard, metav1.CreateOptions{})
|
||||
require.NoError(t, err, "should be able to create v2alpha2 dashboard")
|
||||
|
||||
// Now for the repository.
|
||||
const repo = "local-repository"
|
||||
@@ -642,15 +686,19 @@ func TestProvisioning_ExportUnifiedToRepository(t *testing.T) {
|
||||
title string
|
||||
apiVersion string
|
||||
name string
|
||||
fileName string
|
||||
}
|
||||
|
||||
printFileTree(t, helper.ProvisioningPath)
|
||||
|
||||
// Check that each file was exported with its stored version
|
||||
for _, test := range []props{
|
||||
{title: "Test dashboard. Created at v0", apiVersion: "dashboard.grafana.app/v0alpha1", name: "test-v0"},
|
||||
{title: "Test dashboard. Created at v1", apiVersion: "dashboard.grafana.app/v1beta1", name: "test-v1"},
|
||||
{title: "Test dashboard. Created at v2", apiVersion: "dashboard.grafana.app/v2alpha1", name: "test-v2"},
|
||||
{title: "Test dashboard. Created at v0", apiVersion: "dashboard.grafana.app/v0alpha1", name: "test-v0", fileName: "test-dashboard-created-at-v0.json"},
|
||||
{title: "Test dashboard. Created at v1", apiVersion: "dashboard.grafana.app/v1beta1", name: "test-v1", fileName: "test-dashboard-created-at-v1.json"},
|
||||
{title: "Test dashboard. Created at v2alpha1", apiVersion: "dashboard.grafana.app/v2alpha1", name: "test-v2alpha1", fileName: "test-dashboard-created-at-v2alpha1.json"},
|
||||
{title: "Test dashboard. Created at v2alpha2", apiVersion: "dashboard.grafana.app/v2alpha2", name: "test-v2alpha2", fileName: "test-dashboard-created-at-v2alpha2.json"},
|
||||
} {
|
||||
fpath := filepath.Join(helper.ProvisioningPath, slugify.Slugify(test.title)+".json")
|
||||
fpath := filepath.Join(helper.ProvisioningPath, test.fileName)
|
||||
//nolint:gosec // we are ok with reading files in testdata
|
||||
body, err := os.ReadFile(fpath)
|
||||
require.NoError(t, err, "exported file was not created at path %s", fpath)
|
||||
|
||||
Reference in New Issue
Block a user