2025-02-07 19:04:58 +08:00
package provisioning
import (
"context"
2025-04-03 23:58:05 +08:00
"encoding/json"
2025-03-25 15:59:03 +08:00
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
2025-02-07 19:04:58 +08:00
"testing"
2025-04-12 05:05:41 +08:00
"time"
2025-02-07 19:04:58 +08:00
2025-04-01 18:10:50 +08:00
gh "github.com/google/go-github/v70/github"
2025-03-25 15:59:03 +08:00
ghmock "github.com/migueleliasweb/go-github-mock/src/mock"
"github.com/stretchr/testify/assert"
2025-02-07 19:04:58 +08:00
"github.com/stretchr/testify/require"
2025-04-08 19:17:33 +08:00
apierrors "k8s.io/apimachinery/pkg/api/errors"
2025-02-07 19:04:58 +08:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2025-04-03 23:58:05 +08:00
"github.com/grafana/grafana/pkg/apimachinery/utils"
2025-03-25 15:59:03 +08:00
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/tests/apis"
2025-04-12 05:05:41 +08:00
"k8s.io/apimachinery/pkg/runtime"
2025-03-25 15:59:03 +08:00
)
2025-02-07 19:04:58 +08:00
2025-03-25 15:59:03 +08:00
func TestIntegrationProvisioning_CreatingAndGetting ( t * testing . T ) {
2025-02-07 19:04:58 +08:00
if testing . Short ( ) {
t . Skip ( "skipping integration test" )
}
2025-03-25 15:59:03 +08:00
helper := runGrafana ( t )
createOptions := metav1 . CreateOptions { FieldValidation : "Strict" }
2025-02-07 19:04:58 +08:00
ctx := context . Background ( )
2025-03-25 15:59:03 +08:00
inputFiles := [ ] string {
"testdata/github-readonly.json.tmpl" ,
"testdata/local-readonly.json.tmpl" ,
}
2025-02-07 19:04:58 +08:00
2025-03-25 15:59:03 +08:00
for _ , inputFilePath := range inputFiles {
t . Run ( inputFilePath , func ( t * testing . T ) {
input := helper . RenderObject ( t , inputFilePath , nil )
2025-02-07 19:04:58 +08:00
2025-03-25 15:59:03 +08:00
_ , err := helper . Repositories . Resource . Create ( ctx , input , createOptions )
require . NoError ( t , err , "failed to create resource" )
2025-02-07 19:04:58 +08:00
2025-03-25 15:59:03 +08:00
name := mustNestedString ( input . Object , "metadata" , "name" )
output , err := helper . Repositories . Resource . Get ( ctx , name , metav1 . GetOptions { } )
require . NoError ( t , err , "failed to read back resource" )
2025-02-07 19:04:58 +08:00
2025-03-25 15:59:03 +08:00
// Move encrypted token mutation
token , found , err := unstructured . NestedString ( output . Object , "spec" , "github" , "encryptedToken" )
require . NoError ( t , err , "encryptedToken is not a string" )
if found {
unstructured . RemoveNestedField ( input . Object , "spec" , "github" , "token" )
err = unstructured . SetNestedField ( input . Object , token , "spec" , "github" , "encryptedToken" )
require . NoError ( t , err , "unable to copy encrypted token" )
2025-02-07 19:04:58 +08:00
}
2025-03-25 15:59:03 +08:00
// Marshal as real objects to ",omitempty" values are tested properly
expectedRepo := unstructuredToRepository ( t , input )
returnedRepo := unstructuredToRepository ( t , output )
require . Equal ( t , expectedRepo . Spec , returnedRepo . Spec )
// A viewer should not be able to see the same thing
var statusCode int
rsp := helper . ViewerREST . Get ( ) .
Namespace ( "default" ) .
Resource ( "repositories" ) .
Name ( name ) .
Do ( context . Background ( ) )
require . Error ( t , rsp . Error ( ) )
rsp . StatusCode ( & statusCode )
require . Equal ( t , http . StatusForbidden , statusCode )
// Viewer can see file listing
2025-04-09 22:48:58 +08:00
rsp = helper . AdminREST . Get ( ) .
2025-03-25 15:59:03 +08:00
Namespace ( "default" ) .
Resource ( "repositories" ) .
Name ( name ) .
Suffix ( "files/" ) .
Do ( context . Background ( ) )
require . NoError ( t , rsp . Error ( ) )
} )
}
// Viewer can see settings listing
t . Run ( "viewer has access to list" , func ( t * testing . T ) {
settings := & provisioning . RepositoryViewList { }
rsp := helper . ViewerREST . Get ( ) .
Namespace ( "default" ) .
Suffix ( "settings" ) .
Do ( context . Background ( ) )
require . NoError ( t , rsp . Error ( ) )
err := rsp . Into ( settings )
require . NoError ( t , err )
require . Len ( t , settings . Items , len ( inputFiles ) )
} )
t . Run ( "Repositories are reported in stats" , func ( t * testing . T ) {
report := apis . DoRequest ( helper . K8sTestHelper , apis . RequestParams {
Method : http . MethodGet ,
Path : "/api/admin/usage-report-preview" ,
2025-04-10 20:42:23 +08:00
User : helper . Org1 . Admin ,
2025-03-25 15:59:03 +08:00
} , & usagestats . Report { } )
stats := map [ string ] any { }
for k , v := range report . Result . Metrics {
if strings . HasPrefix ( k , "stats.repository." ) {
stats [ k ] = v
2025-02-07 19:04:58 +08:00
}
}
2025-03-25 15:59:03 +08:00
require . Equal ( t , map [ string ] any {
"stats.repository.github.count" : 1.0 ,
"stats.repository.local.count" : 1.0 ,
} , stats )
} )
}
2025-02-07 19:04:58 +08:00
2025-04-12 05:05:41 +08:00
func TestIntegrationProvisioning_FailInvalidSchema ( t * testing . T ) {
if testing . Short ( ) {
t . Skip ( "skipping integration test" )
}
t . Skip ( "Reenable this test once we enforce schema validation for provisioning" )
helper := runGrafana ( t )
ctx := context . Background ( )
const repo = "invalid-schema-tmp"
// Set up the repository and the file to import.
helper . CopyToProvisioningPath ( t , "testdata/invalid-dashboard-schema.json" , "invalid-dashboard-schema.json" )
localTmp := helper . RenderObject ( t , "testdata/local-write.json.tmpl" , map [ string ] any {
"Name" : repo ,
"SyncEnabled" : true ,
} )
_ , err := helper . Repositories . Resource . Create ( ctx , localTmp , metav1 . CreateOptions { } )
require . NoError ( t , err )
// Make sure the repo can read and validate the file
_ , err = helper . Repositories . Resource . Get ( ctx , repo , metav1 . GetOptions { } , "files" , "invalid-dashboard-schema.json" )
status := helper . RequireApiErrorStatus ( err , metav1 . StatusReasonBadRequest , http . StatusBadRequest )
require . Equal ( t , status . Message , "Dry run failed: Dashboard.dashboard.grafana.app \"invalid-schema-uid\" is invalid: [spec.panels.0.repeatDirection: Invalid value: conflicting values \"h\" and \"this is not an allowed value\", spec.panels.0.repeatDirection: Invalid value: conflicting values \"v\" and \"this is not an allowed value\"]" )
const invalidSchemaUid = "invalid-schema-uid"
_ , err = helper . Dashboards . Resource . Get ( ctx , invalidSchemaUid , metav1 . GetOptions { } )
require . Error ( t , err , "invalid dashboard shouldn't exist" )
require . True ( t , apierrors . IsNotFound ( err ) )
var jobObj * unstructured . Unstructured
assert . EventuallyWithT ( t , func ( collect * assert . CollectT ) {
result := helper . AdminREST . Post ( ) .
Namespace ( "default" ) .
Resource ( "repositories" ) .
Name ( repo ) .
SubResource ( "jobs" ) .
Body ( asJSON ( & provisioning . JobSpec {
Action : provisioning . JobActionPull ,
Pull : & provisioning . SyncJobOptions { } ,
} ) ) .
SetHeader ( "Content-Type" , "application/json" ) .
Do ( t . Context ( ) )
require . NoError ( collect , result . Error ( ) )
job , err := result . Get ( )
require . NoError ( collect , err )
var ok bool
jobObj , ok = job . ( * unstructured . Unstructured )
require . True ( collect , ok , "expecting unstructured object, but got %T" , job )
} , time . Second * 10 , time . Millisecond * 10 , "Expected to be able to start a sync job" )
assert . EventuallyWithT ( t , func ( collect * assert . CollectT ) {
//helper.TriggerJobProcessing(t)
result , err := helper . Repositories . Resource . Get ( ctx , repo , metav1 . GetOptions { } ,
"jobs" , string ( jobObj . GetUID ( ) ) )
if apierrors . IsNotFound ( err ) {
assert . Fail ( collect , "job '%s' not found yet yet" , jobObj . GetName ( ) )
return // continue trying
}
// Can fail fast here -- the jobs are immutable
require . NoError ( t , err )
require . NotNil ( t , result )
job := & provisioning . Job { }
err = runtime . DefaultUnstructuredConverter . FromUnstructured ( result . Object , job )
require . NoError ( t , err , "should convert to Job object" )
require . Equal ( t , provisioning . JobStateError , job . Status . State )
require . Equal ( t , job . Status . Message , "completed with errors" )
require . Equal ( t , job . Status . Errors [ 0 ] , "Dashboard.dashboard.grafana.app \"invalid-schema-uid\" is invalid: [spec.panels.0.repeatDirection: Invalid value: conflicting values \"h\" and \"this is not an allowed value\", spec.panels.0.repeatDirection: Invalid value: conflicting values \"v\" and \"this is not an allowed value\"]" )
} , time . Second * 10 , time . Millisecond * 10 , "Expected provisioning job to conclude with the status failed" )
_ , err = helper . Dashboards . Resource . Get ( ctx , invalidSchemaUid , metav1 . GetOptions { } )
require . Error ( t , err , "invalid dashboard shouldn't have been created" )
require . True ( t , apierrors . IsNotFound ( err ) )
err = helper . Repositories . Resource . Delete ( ctx , repo , metav1 . DeleteOptions { } , "files" , "invalid-dashboard-schema.json" )
require . NoError ( t , err , "should delete the resource file" )
}
2025-03-25 15:59:03 +08:00
func TestIntegrationProvisioning_CreatingGitHubRepository ( t * testing . T ) {
if testing . Short ( ) {
t . Skip ( "skipping integration test" )
2025-02-07 19:04:58 +08:00
}
2025-03-25 15:59:03 +08:00
helper := runGrafana ( t )
ctx := context . Background ( )
2025-02-07 19:04:58 +08:00
2025-03-25 15:59:03 +08:00
helper . GetEnv ( ) . GitHubFactory . Client = ghmock . NewMockedHTTPClient (
ghmock . WithRequestMatchHandler ( ghmock . GetUser , ghAlwaysWrite ( t , & gh . User { Name : gh . Ptr ( "github-user" ) } ) ) ,
ghmock . WithRequestMatchHandler ( ghmock . GetReposHooksByOwnerByRepo , ghAlwaysWrite ( t , [ ] * gh . Hook { } ) ) ,
ghmock . WithRequestMatchHandler ( ghmock . PostReposHooksByOwnerByRepo , ghAlwaysWrite ( t , & gh . Hook { ID : gh . Ptr ( int64 ( 123 ) ) } ) ) ,
ghmock . WithRequestMatchHandler ( ghmock . GetReposByOwnerByRepo , ghAlwaysWrite ( t , & gh . Repository { ID : gh . Ptr ( int64 ( 234 ) ) } ) ) ,
ghmock . WithRequestMatchHandler (
ghmock . GetReposBranchesByOwnerByRepoByBranch ,
ghAlwaysWrite ( t , & gh . Branch {
Name : gh . Ptr ( "main" ) ,
Commit : & gh . RepositoryCommit { SHA : gh . Ptr ( "deadbeef" ) } ,
} ) ,
) ,
ghmock . WithRequestMatchHandler ( ghmock . GetReposGitTreesByOwnerByRepoByTreeSha ,
ghHandleTree ( t , map [ string ] [ ] * gh . TreeEntry {
"deadbeef" : {
treeEntryDir ( "grafana" , "subtree" ) ,
} ,
"subtree" : {
treeEntry ( "dashboard.json" , helper . LoadFile ( "testdata/all-panels.json" ) ) ,
treeEntryDir ( "subdir" , "subtree2" ) ,
treeEntry ( "subdir/dashboard2.yaml" , helper . LoadFile ( "testdata/text-options.json" ) ) ,
} ,
} ) ) ,
ghmock . WithRequestMatchHandler (
ghmock . GetReposContentsByOwnerByRepoByPath ,
http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
pathRegex := regexp . MustCompile ( ` /repos/[^/]+/[^/]+/contents/(.*) ` )
matches := pathRegex . FindStringSubmatch ( r . URL . Path )
require . NotNil ( t , matches , "no match for contents?" )
path := matches [ 1 ]
2025-02-07 19:04:58 +08:00
2025-03-25 15:59:03 +08:00
var err error
switch path {
case "grafana/dashboard.json" :
_ , err = w . Write ( ghmock . MustMarshal ( repoContent ( path , helper . LoadFile ( "testdata/all-panels.json" ) ) ) )
case "grafana/subdir/dashboard2.yaml" :
_ , err = w . Write ( ghmock . MustMarshal ( repoContent ( path , helper . LoadFile ( "testdata/text-options.json" ) ) ) )
default :
t . Fatalf ( "got unexpected path: %s" , path )
}
require . NoError ( t , err )
} ) ,
) ,
)
2025-02-07 19:04:58 +08:00
2025-03-25 15:59:03 +08:00
const repo = "github-create-test"
2025-04-09 15:33:55 +08:00
_ , err := helper . Repositories . Resource . Create ( ctx ,
2025-03-25 15:59:03 +08:00
helper . RenderObject ( t , "testdata/github-readonly.json.tmpl" , map [ string ] any {
"Name" : repo ,
"SyncEnabled" : true ,
"SyncTarget" : "instance" ,
"Path" : "grafana/" ,
} ) ,
2025-04-09 15:33:55 +08:00
metav1 . CreateOptions { } ,
2025-03-25 15:59:03 +08:00
)
require . NoError ( t , err )
2025-02-07 19:04:58 +08:00
2025-03-25 17:41:38 +08:00
helper . SyncAndWait ( t , repo , nil )
2025-03-25 15:59:03 +08:00
// By now, we should have synced, meaning we have data to read in the local Grafana instance!
found , err := helper . Dashboards . Resource . List ( ctx , metav1 . ListOptions { } )
require . NoError ( t , err , "can list values" )
names := [ ] string { }
for _ , v := range found . Items {
names = append ( names , v . GetName ( ) )
}
assert . Contains ( t , names , "n1jR8vnnz" , "should contain dashboard.json's contents" )
assert . Contains ( t , names , "WZ7AhQiVz" , "should contain dashboard2.yaml's contents" )
2025-04-09 15:33:55 +08:00
err = helper . Repositories . Resource . Delete ( ctx , repo , metav1 . DeleteOptions { } )
require . NoError ( t , err , "should delete values" )
t . Run ( "github url cleanup" , func ( t * testing . T ) {
tests := [ ] struct {
name string
input string
output string
} {
{
name : "simple-url" ,
input : "https://github.com/dprokop/grafana-git-sync-test" ,
output : "https://github.com/dprokop/grafana-git-sync-test" ,
} ,
{
name : "trim-dot-git" ,
input : "https://github.com/dprokop/grafana-git-sync-test.git" ,
output : "https://github.com/dprokop/grafana-git-sync-test" ,
} ,
{
name : "trim-slash" ,
input : "https://github.com/dprokop/grafana-git-sync-test/" ,
output : "https://github.com/dprokop/grafana-git-sync-test" ,
} ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
input := helper . RenderObject ( t , "testdata/github-readonly.json.tmpl" , map [ string ] any {
"Name" : test . name ,
"URL" : test . input ,
} )
_ , err := helper . Repositories . Resource . Create ( ctx , input , metav1 . CreateOptions { } )
require . NoError ( t , err , "failed to create resource" )
obj , err := helper . Repositories . Resource . Get ( ctx , test . name , metav1 . GetOptions { } )
require . NoError ( t , err , "failed to read back resource" )
url , _ , err := unstructured . NestedString ( obj . Object , "spec" , "github" , "url" )
require . NoError ( t , err , "failed to read URL" )
require . Equal ( t , test . output , url )
err = helper . Repositories . Resource . Delete ( ctx , test . name , metav1 . DeleteOptions { } )
require . NoError ( t , err , "failed to delete" )
} )
}
} )
2025-03-25 15:59:03 +08:00
}
2025-04-03 23:58:05 +08:00
func TestIntegrationProvisioning_RunLocalRepository ( t * testing . T ) {
2025-03-25 15:59:03 +08:00
if testing . Short ( ) {
t . Skip ( "skipping integration test" )
}
helper := runGrafana ( t )
ctx := context . Background ( )
2025-04-03 23:58:05 +08:00
const allPanels = "n1jR8vnnz"
const repo = "local-local-examples"
const targetPath = "all-panels.json"
2025-03-25 15:59:03 +08:00
// Set up the repository.
localTmp := helper . RenderObject ( t , "testdata/local-write.json.tmpl" , map [ string ] any { "Name" : repo } )
2025-04-03 23:58:05 +08:00
obj , err := helper . Repositories . Resource . Create ( ctx , localTmp , metav1 . CreateOptions { } )
2025-03-25 15:59:03 +08:00
require . NoError ( t , err )
2025-04-03 23:58:05 +08:00
name , _ , _ := unstructured . NestedString ( obj . Object , "metadata" , "name" )
require . Equal ( t , repo , name , "wrote the expected name" )
2025-03-25 15:59:03 +08:00
2025-04-03 23:58:05 +08:00
// Write a file -- this will create it *both* in the local file system, and in grafana
t . Run ( "write all panels" , func ( t * testing . T ) {
code := 0
2025-04-11 18:06:16 +08:00
// Check that we can not (yet) UPDATE the target path
result := helper . AdminREST . Put ( ) .
Namespace ( "default" ) .
Resource ( "repositories" ) .
Name ( repo ) .
SubResource ( "files" , targetPath ) .
Body ( helper . LoadFile ( "testdata/all-panels.json" ) ) .
SetHeader ( "Content-Type" , "application/json" ) .
Do ( ctx ) . StatusCode ( & code )
require . Equal ( t , http . StatusNotFound , code )
require . True ( t , apierrors . IsNotFound ( result . Error ( ) ) )
2025-04-11 22:47:26 +08:00
// Now try again with POST (as an editor)
result = helper . EditorREST . Post ( ) .
2025-04-03 23:58:05 +08:00
Namespace ( "default" ) .
Resource ( "repositories" ) .
Name ( repo ) .
SubResource ( "files" , targetPath ) .
Body ( helper . LoadFile ( "testdata/all-panels.json" ) ) .
SetHeader ( "Content-Type" , "application/json" ) .
Do ( ctx ) . StatusCode ( & code )
require . NoError ( t , result . Error ( ) , "expecting to be able to create file" )
wrapper := & provisioning . ResourceWrapper { }
raw , err := result . Raw ( )
require . NoError ( t , err )
err = json . Unmarshal ( raw , wrapper )
require . NoError ( t , err )
require . Equal ( t , 200 , code , "expected 200 response" )
require . Equal ( t , provisioning . ClassicDashboard , wrapper . Resource . Type . Classic )
name , _ , _ := unstructured . NestedString ( wrapper . Resource . File . Object , "metadata" , "name" )
require . Equal ( t , allPanels , name , "name from classic UID" )
name , _ , _ = unstructured . NestedString ( wrapper . Resource . Upsert . Object , "metadata" , "name" )
require . Equal ( t , allPanels , name , "save the name from the request" )
// Get the file from the grafana database
obj , err := helper . Dashboards . Resource . Get ( ctx , allPanels , metav1 . GetOptions { } )
require . NoError ( t , err , "the value should be saved in grafana" )
val , _ , _ := unstructured . NestedString ( obj . Object , "metadata" , "annotations" , utils . AnnoKeyManagerKind )
require . Equal ( t , string ( utils . ManagerKindRepo ) , val , "should have repo annotations" )
val , _ , _ = unstructured . NestedString ( obj . Object , "metadata" , "annotations" , utils . AnnoKeyManagerIdentity )
require . Equal ( t , repo , val , "should have repo annotations" )
// Read the file we wrote
2025-04-09 22:48:58 +08:00
wrapObj , err := helper . Repositories . Resource . Get ( ctx , repo , metav1 . GetOptions { } , "files" , targetPath )
2025-04-03 23:58:05 +08:00
require . NoError ( t , err , "read value" )
2025-04-09 22:48:58 +08:00
wrap := & unstructured . Unstructured { }
wrap . Object , _ , err = unstructured . NestedMap ( wrapObj . Object , "resource" , "dryRun" )
require . NoError ( t , err )
meta , err := utils . MetaAccessor ( wrap )
require . NoError ( t , err )
require . Equal ( t , allPanels , meta . GetName ( ) , "read the name out of the saved file" )
// Check that an admin can update
meta . SetAnnotation ( "test" , "from-provisioning" )
body , err := json . Marshal ( wrap . Object )
require . NoError ( t , err )
result = helper . AdminREST . Put ( ) .
Namespace ( "default" ) .
Resource ( "repositories" ) .
Name ( repo ) .
SubResource ( "files" , targetPath ) .
Body ( body ) .
SetHeader ( "Content-Type" , "application/json" ) .
Do ( ctx ) . StatusCode ( & code )
require . Equal ( t , 200 , code )
require . NoError ( t , result . Error ( ) , "update as admin value" )
raw , err = result . Raw ( )
require . NoError ( t , err )
err = json . Unmarshal ( raw , wrapper )
require . NoError ( t , err )
anno , _ , _ := unstructured . NestedString ( wrapper . Resource . File . Object , "metadata" , "annotations" , "test" )
require . Equal ( t , "from-provisioning" , anno , "should set the annotation" )
// But a viewer can not
result = helper . ViewerREST . Put ( ) .
Namespace ( "default" ) .
Resource ( "repositories" ) .
Name ( repo ) .
SubResource ( "files" , targetPath ) .
Body ( body ) .
SetHeader ( "Content-Type" , "application/json" ) .
Do ( ctx ) . StatusCode ( & code )
require . Equal ( t , 403 , code )
require . True ( t , apierrors . IsForbidden ( result . Error ( ) ) , code )
2025-04-03 23:58:05 +08:00
} )
2025-03-25 15:59:03 +08:00
2025-04-03 23:58:05 +08:00
t . Run ( "fail using invalid paths" , func ( t * testing . T ) {
result := helper . AdminREST . Post ( ) .
Namespace ( "default" ) .
Resource ( "repositories" ) .
Name ( repo ) .
SubResource ( "files" , "test" , ".." , ".." , "all-panels.json" ) . // UNSAFE PATH
Body ( helper . LoadFile ( "testdata/all-panels.json" ) ) .
SetHeader ( "Content-Type" , "application/json" ) .
Do ( ctx )
require . Error ( t , result . Error ( ) , "invalid path should return error" )
// Read a file with a bad path
_ , err = helper . Repositories . Resource . Get ( ctx , repo , metav1 . GetOptions { } , "files" , "../../all-panels.json" )
require . Error ( t , err , "invalid path should error" )
} )
2025-03-25 15:59:03 +08:00
2025-04-03 23:58:05 +08:00
t . Run ( "require name or generateName" , func ( t * testing . T ) {
code := 0
result := helper . AdminREST . Post ( ) .
Namespace ( "default" ) .
Resource ( "repositories" ) .
Name ( repo ) .
SubResource ( "files" , "example.json" ) .
Body ( [ ] byte ( ` apiVersion : dashboard . grafana . app / v0alpha1
kind : Dashboard
spec :
title : Test dashboard
` ) ) . Do ( ctx ) . StatusCode ( & code )
require . Error ( t , result . Error ( ) , "missing name" )
result = helper . AdminREST . Post ( ) .
Namespace ( "default" ) .
Resource ( "repositories" ) .
Name ( repo ) .
SubResource ( "files" , "example.json" ) .
Body ( [ ] byte ( ` apiVersion : dashboard . grafana . app / v0alpha1
kind : Dashboard
metadata :
generateName : prefix -
spec :
title : Test dashboard
` ) ) . Do ( ctx ) . StatusCode ( & code )
require . NoError ( t , result . Error ( ) , "should create name" )
require . Equal ( t , 200 , code , "expect OK result" )
raw , err := result . Raw ( )
require . NoError ( t , err )
2025-03-25 15:59:03 +08:00
2025-04-03 23:58:05 +08:00
obj := & unstructured . Unstructured { }
err = json . Unmarshal ( raw , obj )
require . NoError ( t , err )
name , _ , _ = unstructured . NestedString ( obj . Object , "resource" , "upsert" , "metadata" , "name" )
require . True ( t , strings . HasPrefix ( name , "prefix-" ) , "should generate name" )
} )
2025-03-25 15:59:03 +08:00
}
func TestIntegrationProvisioning_ImportAllPanelsFromLocalRepository ( t * testing . T ) {
if testing . Short ( ) {
t . Skip ( "skipping integration test" )
}
helper := runGrafana ( t )
ctx := context . Background ( )
const repo = "local-tmp"
// Set up the repository and the file to import.
helper . CopyToProvisioningPath ( t , "testdata/all-panels.json" , "all-panels.json" )
2025-04-08 19:17:33 +08:00
localTmp := helper . RenderObject ( t , "testdata/local-write.json.tmpl" , map [ string ] any {
2025-03-25 15:59:03 +08:00
"Name" : repo ,
"SyncEnabled" : true ,
2025-02-07 19:04:58 +08:00
} )
2025-03-25 15:59:03 +08:00
_ , err := helper . Repositories . Resource . Create ( ctx , localTmp , metav1 . CreateOptions { } )
require . NoError ( t , err )
2025-04-01 22:49:08 +08:00
// Make sure the repo can read and validate the file
obj , err := helper . Repositories . Resource . Get ( ctx , repo , metav1 . GetOptions { } , "files" , "all-panels.json" )
2025-03-25 15:59:03 +08:00
require . NoError ( t , err , "valid path should be fine" )
2025-04-01 22:49:08 +08:00
resource , _ , err := unstructured . NestedMap ( obj . Object , "resource" )
require . NoError ( t , err , "missing resource" )
action , _ , err := unstructured . NestedString ( resource , "action" )
require . NoError ( t , err , "invalid action" )
require . NotNil ( t , resource [ "file" ] , "the raw file" )
require . NotNil ( t , resource [ "dryRun" ] , "dryRun result" )
require . Equal ( t , "create" , action )
2025-03-25 15:59:03 +08:00
// But the dashboard shouldn't exist yet
const allPanels = "n1jR8vnnz"
_ , err = helper . Dashboards . Resource . Get ( ctx , allPanels , metav1 . GetOptions { } )
require . Error ( t , err , "no all-panels dashboard should exist" )
2025-04-08 19:17:33 +08:00
require . True ( t , apierrors . IsNotFound ( err ) )
2025-03-25 15:59:03 +08:00
// Now, we import it, such that it may exist
2025-03-25 17:41:38 +08:00
helper . SyncAndWait ( t , repo , nil )
2025-03-25 15:59:03 +08:00
2025-04-08 19:17:33 +08:00
_ , err = helper . Dashboards . Resource . List ( ctx , metav1 . ListOptions { } )
2025-03-25 15:59:03 +08:00
require . NoError ( t , err , "can list values" )
2025-04-08 19:17:33 +08:00
obj , err = helper . Dashboards . Resource . Get ( ctx , allPanels , metav1 . GetOptions { } )
require . NoError ( t , err , "all-panels dashboard should exist" )
require . Equal ( t , repo , obj . GetAnnotations ( ) [ utils . AnnoKeyManagerIdentity ] )
// Try writing the value directly
err = unstructured . SetNestedField ( obj . Object , [ ] any { "aaa" , "bbb" } , "spec" , "tags" )
require . NoError ( t , err , "set tags" )
_ , err = helper . Dashboards . Resource . Update ( ctx , obj , metav1 . UpdateOptions { } )
require . Error ( t , err , "only the provisionding service should be able to update" )
require . True ( t , apierrors . IsForbidden ( err ) )
// Should not be able to directly delete the managed resource
err = helper . Dashboards . Resource . Delete ( ctx , allPanels , metav1 . DeleteOptions { } )
require . Error ( t , err , "only the provisioning service should be able to delete" )
require . True ( t , apierrors . IsForbidden ( err ) )
// But we can delete the repository file, and this should also remove the resource
err = helper . Repositories . Resource . Delete ( ctx , repo , metav1 . DeleteOptions { } , "files" , "all-panels.json" )
require . NoError ( t , err , "should delete the resource file" )
_ , err = helper . Dashboards . Resource . Get ( ctx , allPanels , metav1 . GetOptions { } )
require . Error ( t , err , "should delete the internal resource" )
require . True ( t , apierrors . IsNotFound ( err ) )
2025-02-07 19:04:58 +08:00
}
2025-03-25 15:59:03 +08:00
func TestProvisioning_ExportUnifiedToRepository ( t * testing . T ) {
if testing . Short ( ) {
t . Skip ( "skipping integration test" )
}
helper := runGrafana ( t )
ctx := context . Background ( )
// Set up dashboards first, then the repository, and finally export.
dashboard := helper . LoadYAMLOrJSONFile ( "exportunifiedtorepository/root_dashboard.json" )
_ , err := helper . Dashboards . Resource . Create ( ctx , dashboard , metav1 . CreateOptions { } )
require . NoError ( t , err , "should be able to create prerequisite dashboard" )
// Now for the repository.
const repo = "local-repository"
createBody := helper . RenderObject ( t , "exportunifiedtorepository/repository.json.tmpl" , map [ string ] any { "Name" : repo } )
_ , err = helper . Repositories . Resource . Create ( ctx , createBody , metav1 . CreateOptions { } )
require . NoError ( t , err , "should be able to create repository" )
// Now export...
result := helper . AdminREST . Post ( ) .
Namespace ( "default" ) .
Resource ( "repositories" ) .
Name ( repo ) .
2025-04-01 18:22:47 +08:00
SubResource ( "jobs" ) .
2025-03-25 15:59:03 +08:00
SetHeader ( "Content-Type" , "application/json" ) .
2025-04-01 18:22:47 +08:00
Body ( asJSON ( & provisioning . JobSpec {
Push : & provisioning . ExportJobOptions {
2025-04-03 23:58:05 +08:00
Folder : "" , // export entire instance
Path : "" , // no prefix necessary for testing
2025-04-01 18:22:47 +08:00
} ,
2025-03-25 15:59:03 +08:00
} ) ) .
Do ( ctx )
require . NoError ( t , result . Error ( ) )
// And time to assert.
helper . AwaitJobs ( t , repo )
fpath := filepath . Join ( helper . ProvisioningPath , slugify . Slugify ( mustNestedString ( dashboard . Object , "spec" , "title" ) ) + ".json" )
_ , err = os . Stat ( fpath )
require . NoError ( t , err , "exported file was not created at path %s" , fpath )
2025-02-07 19:04:58 +08:00
}