Files
grafana/pkg/tests/apis/dashboard/integration/api_validation_test.go

840 lines
28 KiB
Go

package integration
import (
"context"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
dashboardv1alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1"
folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/dashboards" // TODO: Check if we can remove this import
"github.com/grafana/grafana/pkg/services/quota"
)
func TestMain(m *testing.M) {
testsuite.Run(m)
}
// TestContext holds common test resources
type TestContext struct {
Helper *apis.K8sTestHelper
AdminUser apis.User
EditorUser apis.User
ViewerUser apis.User
TestFolder *folder.Folder
AdminServiceAccountToken string
EditorServiceAccountToken string
ViewerServiceAccountToken string
OrgID int64
}
// TestIntegrationValidation tests the dashboard K8s API
func TestIntegrationValidation(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
// Create a K8sTestHelper which will set up a real API server
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
DisableAnonymous: true,
EnableFeatureToggles: []string{
featuremgmt.FlagKubernetesClientDashboardsFolders, // Enable dashboard feature
},
})
t.Cleanup(func() {
helper.Shutdown()
})
// Create test contexts organization
org1Ctx := createTestContext(t, helper, helper.Org1)
t.Run("Organization 1 tests", func(t *testing.T) {
t.Run("Dashboard validation tests", func(t *testing.T) {
runDashboardValidationTests(t, org1Ctx)
})
t.Run("Dashboard quota tests", func(t *testing.T) {
runQuotaTests(t, org1Ctx)
})
})
}
// Auth identity types (user or token) with resource client
type Identity struct {
Name string
DashboardClient *apis.K8sResourceClient
FolderClient *apis.K8sResourceClient
Type string // "user" or "token"
}
// TODO: Test plugin dashboard updates with and without overwrite flag
// Run tests for dashboard validations
func runDashboardValidationTests(t *testing.T, ctx TestContext) {
t.Helper()
adminClient := getResourceClient(t, ctx.Helper, ctx.AdminUser, getDashboardGVR())
editorClient := getResourceClient(t, ctx.Helper, ctx.EditorUser, getDashboardGVR())
t.Run("Dashboard UID validations", func(t *testing.T) {
// Test creating dashboard with existing UID
t.Run("reject dashboard with existing UID", func(t *testing.T) {
// Create a dashboard with a specific UID
specificUID := "existing-uid-dash"
createdDash, err := createDashboard(t, adminClient, "Dashboard with Specific UID", nil, &specificUID)
require.NoError(t, err)
// Try to create another dashboard with the same UID
_, err = createDashboard(t, adminClient, "Another Dashboard with Same UID", nil, &specificUID)
require.Error(t, err)
// Clean up
err = adminClient.Resource.Delete(context.Background(), createdDash.GetName(), v1.DeleteOptions{})
require.NoError(t, err)
})
// Test creating dashboard with too long UID
t.Run("reject dashboard with too long UID", func(t *testing.T) {
// Create a dashboard with a long UID (over 40 chars)
longUID := "this-uid-is-way-too-long-for-a-dashboard-uid-12345678901234567890"
_, err := createDashboard(t, adminClient, "Dashboard with Long UID", nil, &longUID)
require.Error(t, err)
})
// Test creating dashboard with invalid UID characters
t.Run("reject dashboard with invalid UID characters", func(t *testing.T) {
invalidUID := "invalid/uid/with/slashes"
_, err := createDashboard(t, adminClient, "Dashboard with Invalid UID", nil, &invalidUID)
require.Error(t, err)
})
})
// TODO: Validate both at creation and update
t.Run("Dashboard title validations", func(t *testing.T) {
// Test empty title
t.Run("reject dashboard with empty title", func(t *testing.T) {
_, err := createDashboard(t, adminClient, "", nil, nil)
require.Error(t, err)
})
// Test long title
t.Run("reject dashboard with excessively long title", func(t *testing.T) {
veryLongTitle := strings.Repeat("a", 10000)
_, err := createDashboard(t, adminClient, veryLongTitle, nil, nil)
require.Error(t, err)
})
// Test updating dashboard with empty title
t.Run("reject dashboard update with empty title", func(t *testing.T) {
// First create a valid dashboard
dash, err := createDashboard(t, adminClient, "Valid Dashboard Title", nil, nil)
require.NoError(t, err)
require.NotNil(t, dash)
// Try to update with empty title
_, err = updateDashboard(t, adminClient, dash, "", nil)
require.Error(t, err)
// Clean up
err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{})
require.NoError(t, err)
})
// Test updating dashboard with excessively long title
t.Run("reject dashboard update with excessively long title", func(t *testing.T) {
// First create a valid dashboard
dash, err := createDashboard(t, adminClient, "Valid Dashboard Title", nil, nil)
require.NoError(t, err)
require.NotNil(t, dash)
// Try to update with excessively long title
veryLongTitle := strings.Repeat("a", 10000)
_, err = updateDashboard(t, adminClient, dash, veryLongTitle, nil)
require.Error(t, err)
// Clean up
err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{})
require.NoError(t, err)
})
})
t.Run("Dashboard message validations", func(t *testing.T) {
// Test long message
t.Run("reject dashboard with excessively long update message", func(t *testing.T) {
dash, err := createDashboard(t, adminClient, "Regular dashboard", nil, nil)
require.NoError(t, err)
veryLongMessage := strings.Repeat("a", 600)
_, err = updateDashboard(t, adminClient, dash, "Dashboard updated with a long message", &veryLongMessage)
require.Error(t, err)
// Clean up
err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{})
require.NoError(t, err)
})
})
t.Run("Dashboard schema validations", func(t *testing.T) {
// Test invalid dashboard schema
t.Run("reject dashboard with invalid schema", func(t *testing.T) {
dashObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": dashboardv1alpha1.DashboardResourceInfo.GroupVersion().String(),
"kind": dashboardv1alpha1.DashboardResourceInfo.GroupVersionKind().Kind,
"metadata": map[string]interface{}{
"generateName": "test-",
},
// Missing spec
},
}
_, err := adminClient.Resource.Create(context.Background(), dashObj, v1.CreateOptions{})
require.Error(t, err)
})
})
t.Run("Dashboard version handling", func(t *testing.T) {
// Test version increment on update
t.Run("version increments on dashboard update", func(t *testing.T) {
// Create a dashboard with admin
dash, err := createDashboard(t, adminClient, "Dashboard for Version Test", nil, nil)
require.NoError(t, err, "Failed to create dashboard for version test")
dashUID := dash.GetName()
// Get the initial version
meta, _ := utils.MetaAccessor(dash)
initialGeneration := meta.GetGeneration()
initialRV := meta.GetResourceVersion()
// Update the dashboard
updatedDash, err := updateDashboard(t, adminClient, dash, "Updated Dashboard for Version Test", nil)
require.NoError(t, err)
require.NotNil(t, updatedDash)
// Check that version was incremented
meta, _ = utils.MetaAccessor(updatedDash)
require.Greater(t, meta.GetGeneration(), initialGeneration, "Generation should be incremented after update")
require.NotEqual(t, meta.GetResourceVersion(), initialRV, "Resource version should be changed after update")
// Clean up
err = adminClient.Resource.Delete(context.Background(), dashUID, v1.DeleteOptions{})
require.NoError(t, err)
})
// Test generation conflict when updating concurrently
t.Run("reject update with version conflict", func(t *testing.T) {
// Create a dashboard with admin
dash, err := createDashboard(t, adminClient, "Dashboard for Version Conflict Test", nil, nil)
require.NoError(t, err, "Failed to create dashboard for version conflict test")
dashUID := dash.GetName()
// Get the dashboard twice (simulating two users getting it)
dash1, err := adminClient.Resource.Get(context.Background(), dashUID, v1.GetOptions{})
require.NoError(t, err)
dash2, err := editorClient.Resource.Get(context.Background(), dashUID, v1.GetOptions{})
require.NoError(t, err)
// Update with the first copy
updatedDash1, err := updateDashboard(t, adminClient, dash1, "Updated by first user", nil)
require.NoError(t, err)
require.NotNil(t, updatedDash1)
// Try to update with the second copy (should fail with version conflict)
_, err = updateDashboard(t, editorClient, dash2, "Updated by second user", nil)
require.Error(t, err)
require.Contains(t, err.Error(), "the object has been modified", "Should fail with version conflict error")
// Clean up
err = adminClient.Resource.Delete(context.Background(), dashUID, v1.DeleteOptions{})
require.NoError(t, err)
})
// Test setting an explicit generation
t.Run("explicit generation setting is validated", func(t *testing.T) {
t.Skip("Double check expected behavior")
// Create a dashboard with a specific generation
dashObj := createDashboardObject(t, "Dashboard with Explicit Generation", "", 0)
meta, _ := utils.MetaAccessor(dashObj)
meta.SetGeneration(5)
// Create the dashboard
createdDash, err := adminClient.Resource.Create(context.Background(), dashObj, v1.CreateOptions{})
require.NoError(t, err)
dashUID := createdDash.GetName()
// Fetch the created dashboard
fetchedDash, err := adminClient.Resource.Get(context.Background(), dashUID, v1.GetOptions{})
require.NoError(t, err)
// Verify the generation was handled properly
meta, _ = utils.MetaAccessor(fetchedDash)
require.Equal(t, 5, meta.GetGeneration(), "Generation should be 5")
// Clean up
err = adminClient.Resource.Delete(context.Background(), dashUID, v1.DeleteOptions{})
require.NoError(t, err)
})
})
t.Run("Dashboard provisioning validations", func(t *testing.T) {
t.Skip("TODO: We need to create provisioned dashboards in two different ways to test this")
// Test updating provisioned dashboard
testCases := []struct {
name string
allowsEdits bool
shouldSucceed bool
}{
{
name: "reject updating provisioned dashboard when allowsEdits is false",
allowsEdits: false,
shouldSucceed: false,
},
{
name: "allow updating provisioned dashboard when allowsEdits is true",
allowsEdits: true,
shouldSucceed: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a dashboard with admin
dash, err := createDashboard(t, adminClient, "Dashboard for Provisioning Test", nil, nil)
require.NoError(t, err, "Failed to create dashboard for provisioning test")
dashUID := dash.GetName()
// Fetch the created dashboard
fetchedDash, err := adminClient.Resource.Get(context.Background(), dashUID, v1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, fetchedDash)
// Mark the dashboard as provisioned with allowsEdits parameter
provisionedDash := markDashboardObjectAsProvisioned(t, fetchedDash, "test-provider", "test-external-id", "test-checksum", tc.allowsEdits)
// Update the dashboard to apply the provisioning annotations
updatedDash, err := adminClient.Resource.Update(context.Background(), provisionedDash, v1.UpdateOptions{})
require.NoError(t, err)
require.NotNil(t, updatedDash)
// Re-fetch the dashboard after it's marked as provisioned
provisionedFetchedDash, err := editorClient.Resource.Get(context.Background(), dashUID, v1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, provisionedFetchedDash)
// Try to update the dashboard using editor (not admin)
dashThatShouldFail, err := updateDashboard(t, editorClient, provisionedFetchedDash, "Updated Provisioned Dashboard", nil)
_ = dashThatShouldFail
if tc.shouldSucceed {
require.NoError(t, err, "Editor should be able to update provisioned dashboard when allowsEdits is true")
// Verify the update succeeded by fetching the dashboard again
updatedDash, err := editorClient.Resource.Get(context.Background(), dashUID, v1.GetOptions{})
require.NoError(t, err)
meta, _ := utils.MetaAccessor(updatedDash)
require.Equal(t, "Updated Provisioned Dashboard", meta.FindTitle(""), "Dashboard title should be updated")
} else {
require.Error(t, err, "Editor should not be able to update provisioned dashboard when allowsEdits is false")
require.Contains(t, err.Error(), "provisioned")
}
// Clean up
err = adminClient.Resource.Delete(context.Background(), dashUID, v1.DeleteOptions{})
require.NoError(t, err)
})
}
})
t.Run("Dashboard refresh interval validations", func(t *testing.T) {
// Create test client
adminClient := getResourceClient(t, ctx.Helper, ctx.AdminUser, getDashboardGVR())
// Store original settings to restore after test
origCfg := ctx.Helper.GetEnv().Cfg
origMinRefreshInterval := origCfg.MinRefreshInterval
// Set a fixed min_refresh_interval for all tests to make them predictable
ctx.Helper.GetEnv().Cfg.MinRefreshInterval = "10s"
testCases := []struct {
name string
refreshValue string
shouldSucceed bool
}{
{
name: "reject dashboard with refresh interval below minimum",
refreshValue: "5s",
shouldSucceed: false,
},
{
name: "accept dashboard with refresh interval equal to minimum",
refreshValue: "10s",
shouldSucceed: true,
},
{
name: "accept dashboard with refresh interval above minimum",
refreshValue: "30s",
shouldSucceed: true,
},
{
name: "accept dashboard with auto refresh",
refreshValue: "auto",
shouldSucceed: true,
},
{
name: "accept dashboard with empty refresh",
refreshValue: "",
shouldSucceed: true,
},
{
name: "reject dashboard with invalid refresh format",
refreshValue: "invalid",
shouldSucceed: false,
},
}
for _, tc := range testCases {
tc := tc // Capture for parallel execution
t.Run(tc.name, func(t *testing.T) {
// Create the dashboard with the specified refresh value
dashObj := createDashboardObject(t, "Dashboard with Refresh: "+tc.refreshValue, "", 0)
// Add refresh configuration using MetaAccessor
meta, _ := utils.MetaAccessor(dashObj)
spec, _ := meta.GetSpec()
specMap := spec.(map[string]interface{})
specMap["refresh"] = tc.refreshValue
_ = meta.SetSpec(specMap)
dash, err := adminClient.Resource.Create(context.Background(), dashObj, v1.CreateOptions{})
if tc.shouldSucceed {
require.NoError(t, err)
require.NotNil(t, dash)
// Clean up
err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{})
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
// Restore original settings
ctx.Helper.GetEnv().Cfg.MinRefreshInterval = origMinRefreshInterval
})
t.Run("Dashboard size limit validations", func(t *testing.T) {
t.Run("reject dashboard exceeding size limit", func(t *testing.T) {
t.Skip("Skipping size limit test for now") // TODO: Revisit this.
// Create a dashboard with a specific UID to make it easier to manage
specificUID := "size-limit-test-dash"
dash, err := createDashboard(t, adminClient, "Dashboard Exceeding Size Limit", nil, &specificUID)
require.NoError(t, err)
meta, _ := utils.MetaAccessor(dash)
spec, _ := meta.GetSpec()
specMap := spec.(map[string]interface{})
// Create a large number of panels
var largePanelArray []map[string]interface{}
// Create 500000 simple panels with unique IDs (to exceed max allowed request size)
for i := 0; i < 500000; i++ {
// Create a simple panel with minimal properties
panel := map[string]interface{}{
"id": i,
"type": "graph",
"title": fmt.Sprintf("Panel %d", i),
"description": fmt.Sprintf("Panel description %d", i),
"gridPos": map[string]interface{}{
"h": 8,
"w": 12,
"x": i % 24,
"y": (i / 24) * 8,
},
"targets": []map[string]interface{}{
{
"refId": "A",
"expr": fmt.Sprintf("metric%d", i),
},
},
}
largePanelArray = append(largePanelArray, panel)
}
specMap["panels"] = largePanelArray
err = meta.SetSpec(specMap)
require.NoError(t, err, "Failed to set spec")
// Try to update with too many panels
_, err = adminClient.Resource.Update(context.Background(), dash, v1.UpdateOptions{})
require.Error(t, err)
require.Contains(t, err.Error(), "exceeds", "Error should mention size or limit exceeded")
// Clean up
err = adminClient.Resource.Delete(context.Background(), specificUID, v1.DeleteOptions{})
require.NoError(t, err)
})
})
}
// Run tests for quota validation
func runQuotaTests(t *testing.T, ctx TestContext) {
t.Helper()
t.Skip("Skipping quota tests for now")
// TODO: Check why we return quota.disabled and also make sure we are able to handle it.
// Get access to services - use the helper environment's HTTP server
quotaService := ctx.Helper.GetEnv().Server.HTTPServer.QuotaService
require.NotNil(t, quotaService, "Quota service should be available")
adminClient := getResourceClient(t, ctx.Helper, ctx.AdminUser, getDashboardGVR())
adminUserId, err := identity.UserIdentifier(ctx.AdminUser.Identity.GetID())
require.NoError(t, err)
// Define quota test cases
testCases := []struct {
name string
scope quota.Scope
id int64
scopeParam func(cmd *quota.UpdateQuotaCmd)
}{
{
name: "Organization quota",
scope: quota.OrgScope,
id: ctx.OrgID,
scopeParam: func(cmd *quota.UpdateQuotaCmd) {
cmd.OrgID = ctx.OrgID
},
},
{
name: "User quota",
scope: quota.UserScope,
id: adminUserId,
scopeParam: func(cmd *quota.UpdateQuotaCmd) {
cmd.UserID = adminUserId
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Get current quotas
quotas, err := quotaService.GetQuotasByScope(context.Background(), tc.scope, tc.id)
require.NoError(t, err, "Failed to get quotas")
// Find the dashboard quota and save original value
var originalQuota int64 = -1 // Default if not found
var quotaFound bool
for _, q := range quotas {
if q.Target == string(dashboards.QuotaTarget) {
originalQuota = q.Limit
quotaFound = true
break
}
}
// Set quota to 1 dashboard
updateCmd := &quota.UpdateQuotaCmd{
Target: string(dashboards.QuotaTarget),
Limit: 1,
}
tc.scopeParam(updateCmd)
err = quotaService.Update(context.Background(), updateCmd)
require.NoError(t, err, "Failed to update quota")
// Create first dashboard - should succeed
dash1, err := createDashboard(t, adminClient, fmt.Sprintf("Quota Test Dashboard 1 (%s)", tc.name), nil, nil)
require.NoError(t, err, "Failed to create first dashboard")
// Create second dashboard - should fail due to quota
_, err = createDashboard(t, adminClient, fmt.Sprintf("Quota Test Dashboard 2 (%s)", tc.name), nil, nil)
require.Error(t, err, "Creating second dashboard should fail due to quota")
require.Contains(t, err.Error(), "quota", "Error should mention quota")
// Clean up the dashboard to reset the quota usage
err = adminClient.Resource.Delete(context.Background(), dash1.GetName(), v1.DeleteOptions{})
require.NoError(t, err, "Failed to delete test dashboard")
// Restore the original quota state
if quotaFound {
// If quota existed originally, restore its value
resetCmd := &quota.UpdateQuotaCmd{
Target: string(dashboards.QuotaTarget),
Limit: originalQuota,
}
tc.scopeParam(resetCmd)
err = quotaService.Update(context.Background(), resetCmd)
require.NoError(t, err, "Failed to reset quota")
} else if tc.scope == quota.UserScope {
// If user quota didn't exist originally, delete it
err = quotaService.DeleteQuotaForUser(context.Background(), tc.id)
require.NoError(t, err, "Failed to delete user quota")
}
})
}
}
// Helper function to create test context for an organization
func createTestContext(t *testing.T, helper *apis.K8sTestHelper, orgUsers apis.OrgUsers) TestContext {
// Create test folder
folderTitle := "Test Folder " + orgUsers.Admin.Identity.GetLogin()
testFolder, err := createFolder(t, helper, orgUsers.Admin, folderTitle)
require.NoError(t, err, "Failed to create test folder")
// Create test context
return TestContext{
Helper: helper,
AdminUser: orgUsers.Admin,
EditorUser: orgUsers.Editor,
ViewerUser: orgUsers.Viewer,
TestFolder: testFolder,
AdminServiceAccountToken: orgUsers.AdminServiceAccountToken,
EditorServiceAccountToken: orgUsers.EditorServiceAccountToken,
ViewerServiceAccountToken: orgUsers.ViewerServiceAccountToken,
OrgID: orgUsers.Admin.Identity.GetOrgID(),
}
}
// getDashboardGVR returns the dashboard GroupVersionResource
func getDashboardGVR() schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: dashboardv1alpha1.DashboardResourceInfo.GroupVersion().Group,
Version: dashboardv1alpha1.DashboardResourceInfo.GroupVersion().Version,
Resource: dashboardv1alpha1.DashboardResourceInfo.GetName(),
}
}
// getFolderGVR returns the folder GroupVersionResource
func getFolderGVR() schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: folderv0alpha1.FolderResourceInfo.GroupVersion().Group,
Version: folderv0alpha1.FolderResourceInfo.GroupVersion().Version,
Resource: folderv0alpha1.FolderResourceInfo.GetName(),
}
}
// Get a resource client for the specified user
func getResourceClient(t *testing.T, helper *apis.K8sTestHelper, user apis.User, gvr schema.GroupVersionResource) *apis.K8sResourceClient {
t.Helper()
return helper.GetResourceClient(apis.ResourceClientArgs{
User: user,
Namespace: helper.Namespacer(user.Identity.GetOrgID()),
GVR: gvr,
})
}
// Get a resource client for the specified service token
// nolint:unused
func getServiceAccountResourceClient(t *testing.T, helper *apis.K8sTestHelper, token string, orgID int64, gvr schema.GroupVersionResource) *apis.K8sResourceClient {
t.Helper()
return helper.GetResourceClient(apis.ResourceClientArgs{
ServiceAccountToken: token,
Namespace: helper.Namespacer(orgID),
GVR: gvr,
})
}
// Create a folder object for testing
func createFolderObject(t *testing.T, title string, namespace string, parentFolderUID string) *unstructured.Unstructured {
t.Helper()
folderObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": folderv0alpha1.FolderResourceInfo.GroupVersion().String(),
"kind": folderv0alpha1.FolderResourceInfo.GroupVersionKind().Kind,
"metadata": map[string]interface{}{
"generateName": "test-folder-",
"namespace": namespace,
},
"spec": map[string]interface{}{
"title": title,
},
},
}
if parentFolderUID != "" {
meta, _ := utils.MetaAccessor(folderObj)
meta.SetFolder(parentFolderUID)
}
return folderObj
}
// Create a folder using Kubernetes API
func createFolder(t *testing.T, helper *apis.K8sTestHelper, user apis.User, title string) (*folder.Folder, error) {
t.Helper()
// Get a client for the folder resource
folderClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: user,
Namespace: helper.Namespacer(user.Identity.GetOrgID()),
GVR: getFolderGVR(),
})
// Create a folder resource
folderObj := createFolderObject(t, title, helper.Namespacer(user.Identity.GetOrgID()), "")
// Create the folder using the K8s client
ctx := context.Background()
createdFolder, err := folderClient.Resource.Create(ctx, folderObj, v1.CreateOptions{})
if err != nil {
return nil, err
}
meta, _ := utils.MetaAccessor(createdFolder)
// Create a folder struct to return (for compatibility with existing code)
return &folder.Folder{
UID: createdFolder.GetName(),
Title: meta.FindTitle(""),
}, nil
}
// Create a dashboard object for testing
func createDashboardObject(t *testing.T, title string, folderUID string, generation int64) *unstructured.Unstructured {
t.Helper()
dashObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": dashboardv1alpha1.DashboardResourceInfo.GroupVersion().String(),
"kind": dashboardv1alpha1.DashboardResourceInfo.GroupVersionKind().Kind,
"metadata": map[string]interface{}{
"generateName": "test-",
},
"spec": map[string]interface{}{
"title": title,
},
},
}
// Get the metadata accessor
meta, err := utils.MetaAccessor(dashObj)
require.NoError(t, err, "Failed to get metadata accessor")
// Get the dashboard's spec
spec, err := meta.GetSpec()
require.NoError(t, err, "Failed to get spec")
specMap := spec.(map[string]interface{})
if folderUID != "" {
meta.SetFolder(folderUID)
}
if generation > 0 {
meta.SetGeneration(generation)
}
// Update the spec
err = meta.SetSpec(specMap)
require.NoError(t, err, "Failed to set spec")
return dashObj
}
// Mark dashboard object as provisioned by setting appropriate annotations
func markDashboardObjectAsProvisioned(t *testing.T, dashboard *unstructured.Unstructured, providerName string, externalID string, checksum string, allowsEdits bool) *unstructured.Unstructured {
meta, err := utils.MetaAccessor(dashboard)
require.NoError(t, err)
m := utils.ManagerProperties{}
s := utils.SourceProperties{}
m.Kind = utils.ManagerKindKubectl
m.Identity = providerName
m.AllowsEdits = allowsEdits
s.Path = externalID
s.Checksum = checksum
s.TimestampMillis = 1633046400000
meta.SetManagerProperties(m)
meta.SetSourceProperties(s)
return dashboard
}
// Create a dashboard
func createDashboard(t *testing.T, client *apis.K8sResourceClient, title string, folderUID *string, uid *string) (*unstructured.Unstructured, error) {
t.Helper()
var folderUIDStr string
if folderUID != nil && *folderUID != "" {
folderUIDStr = *folderUID
}
dashObj := createDashboardObject(t, title, folderUIDStr, 0)
// Set the name (UID) if provided
if uid != nil && *uid != "" {
meta, _ := utils.MetaAccessor(dashObj)
meta.SetName(*uid)
// Remove generateName if we're explicitly setting a name
delete(dashObj.Object["metadata"].(map[string]interface{}), "generateName")
}
// Create the dashboard
createdDash, err := client.Resource.Create(context.Background(), dashObj, v1.CreateOptions{})
if err != nil {
return nil, err
}
// TODO: Remove once the underlying issue is fixed:
// https://raintank-corp.slack.com/archives/C05FYAPEPKP/p1743111830777889
// This only happens in mode 0.
databaseDash, err := client.Resource.Get(context.Background(), createdDash.GetName(), v1.GetOptions{})
if err != nil {
return nil, err
}
require.NotEqual(t, createdDash.GetUID(), databaseDash.GetUID(), "The underlying UID mismatch bug has been fixed, please remove the redundant read!")
return databaseDash, nil
}
// Update a dashboard
func updateDashboard(t *testing.T, client *apis.K8sResourceClient, dashboard *unstructured.Unstructured, newTitle string, updateMessage *string) (*unstructured.Unstructured, error) {
t.Helper()
meta, _ := utils.MetaAccessor(dashboard)
// Get the spec using MetaAccessor
dashSpec, _ := meta.GetSpec()
specMap := dashSpec.(map[string]interface{})
// Update the title
specMap["title"] = newTitle
// Set the updated spec
_ = meta.SetSpec(specMap)
// Set message if provided
if updateMessage != nil {
meta.SetMessage(*updateMessage)
}
// Update the dashboard
return client.Resource.Update(context.Background(), dashboard, v1.UpdateOptions{})
}