@ -7,12 +7,8 @@ import (
mock "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynamicfake "k8s.io/client-go/dynamic/fake"
k8testing "k8s.io/client-go/testing"
provisioningV0 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
@ -20,44 +16,35 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
)
func TestExportResources ( t * testing . T ) {
func TestExportResources_Dashboards ( t * testing . T ) {
tests := [ ] struct {
name string
reactorFunc func ( action k8testing . Action ) ( bool , runtime . Object , error )
mockItems [ ] unstructured . Unstructured
expectedError string
setupProgress func ( progress * jobs . MockJobProgressRecorder )
setupResources func ( repoResources * resources . MockRepositoryResources , resourceClients * resources . MockResourceClients , dynamicClient * dynamicfake . FakeDynamicClient , gvk schema . GroupVersionKind )
setupResources func ( repoResources * resources . MockRepositoryResources , resourceClients * resources . MockResourceClients , mockClient * mockDynamicInterface , gvk schema . GroupVersionKind )
} {
{
name : "successful dashboard export" ,
reactorFunc : func ( action k8testing . Action ) ( bool , runtime . Object , error ) {
// Return dashboard list
return true , & metav1 . PartialObjectMetadataList {
TypeMeta : metav1 . TypeMeta {
APIVersion : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
Kind : "DashboardList" ,
} ,
Items : [ ] metav1 . PartialObjectMetadata {
{
TypeMeta : metav1 . TypeMeta {
APIVersion : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
Kind : "Dashboard" ,
} ,
ObjectMeta : metav1 . ObjectMeta {
Name : "dashboard-1" ,
} ,
mockItems : [ ] unstructured . Unstructured {
{
Object : map [ string ] interface { } {
"apiVersion" : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
"kind" : "Dashboard" ,
"metadata" : map [ string ] interface { } {
"name" : "dashboard-1" ,
} ,
{
TypeMeta : metav1 . TypeMeta {
APIVersion : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
Kind : "Dashboard" ,
} ,
ObjectMeta : metav1 . ObjectMeta {
Name : "dashboard-2" ,
} ,
} ,
} ,
{
Object : map [ string ] interface { } {
"apiVersion" : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
"kind" : "Dashboard" ,
"metadata" : map [ string ] interface { } {
"name" : "dashboard-2" ,
} ,
} ,
} , nil
} ,
} ,
expectedError : "" ,
setupProgress : func ( progress * jobs . MockJobProgressRecorder ) {
@ -72,8 +59,8 @@ func TestExportResources(t *testing.T) {
progress . On ( "TooManyErrors" ) . Return ( nil )
progress . On ( "TooManyErrors" ) . Return ( nil )
} ,
setupResources : func ( repoResources * resources . MockRepositoryResources , resourceClients * resources . MockResourceClients , dynamicClient * dynamicfake . FakeDynamicClient , gvk schema . GroupVersionKind ) {
resourceClients . On ( "ForResource" , resources . DashboardResource ) . Return ( dynamicClient . Resource ( resources . DashboardResource ) , gvk , 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" ,
@ -89,62 +76,38 @@ func TestExportResources(t *testing.T) {
} ,
} ,
{
name : "client error" ,
reactorFunc : func ( action k8testing . Action ) ( bool , runtime . Object , error ) {
return true , nil , fmt . Errorf ( "shouldn't happen" )
} ,
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 , dynamicClient * dynamicfake . FakeDynamicClient , gvk schema . GroupVersionKind ) {
resourceClients . On ( "ForResource" , resources . DashboardResource ) . Return ( dynamicClient . Resource ( resources . DashboardResource ) , gvk , fmt . Errorf ( "didn't work" ) )
} ,
} ,
{
name : "dashboard list error" ,
reactorFunc : func ( action k8testing . Action ) ( bool , runtime . Object , error ) {
return true , nil , fmt . Errorf ( "failed to list dashboards" )
} ,
expectedError : "export dashboards: error executing list: failed to list dashboards" ,
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 , dynamicClient * dynamicfake . FakeDynamicClient , gvk schema . GroupVersionKind ) {
resourceClients . On ( "ForResource" , resources . DashboardResource ) . Return ( dynamicClient . Resource ( resources . DashboardResource ) , gvk , nil )
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" ,
reactorFunc : func ( action k8testing . Action ) ( bool , runtime . Object , error ) {
return true , & metav1 . PartialObjectMetadataList {
TypeMeta : metav1 . TypeMeta {
APIVersion : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
Kind : "DashboardList" ,
} ,
Items : [ ] metav1 . PartialObjectMetadata {
{
TypeMeta : metav1 . TypeMeta {
APIVersion : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
Kind : "Dashboard" ,
} ,
ObjectMeta : metav1 . ObjectMeta {
Name : "dashboard-1" ,
} ,
mockItems : [ ] unstructured . Unstructured {
{
Object : map [ string ] interface { } {
"apiVersion" : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
"kind" : "Dashboard" ,
"metadata" : map [ string ] interface { } {
"name" : "dashboard-1" ,
} ,
{
TypeMeta : metav1 . TypeMeta {
APIVersion : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
Kind : "Dashboard" ,
} ,
ObjectMeta : metav1 . ObjectMeta {
Name : "dashboard-2" ,
} ,
} ,
} ,
{
Object : map [ string ] interface { } {
"apiVersion" : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
"kind" : "Dashboard" ,
"metadata" : map [ string ] interface { } {
"name" : "dashboard-2" ,
} ,
} ,
} , nil
} ,
} ,
expectedError : "" ,
setupProgress : func ( progress * jobs . MockJobProgressRecorder ) {
@ -159,8 +122,8 @@ func TestExportResources(t *testing.T) {
progress . On ( "TooManyErrors" ) . Return ( nil )
progress . On ( "TooManyErrors" ) . Return ( nil )
} ,
setupResources : func ( repoResources * resources . MockRepositoryResources , resourceClients * resources . MockResourceClients , dynamicClient * dynamicfake . FakeDynamicClient , gvk schema . GroupVersionKind ) {
resourceClients . On ( "ForResource" , resources . DashboardResource ) . Return ( dynamicClient . Resource ( resources . DashboardResource ) , gvk , 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" ,
@ -177,24 +140,16 @@ func TestExportResources(t *testing.T) {
} ,
{
name : "dashboard export too many errors" ,
reactorFunc : func ( action k8testing . Action ) ( bool , runtime . Object , error ) {
return true , & metav1 . PartialObjectMetadataList {
TypeMeta : metav1 . TypeMeta {
APIVersion : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
Kind : "DashboardList" ,
} ,
Items : [ ] metav1 . PartialObjectMetadata {
{
TypeMeta : metav1 . TypeMeta {
APIVersion : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
Kind : "Dashboard" ,
} ,
ObjectMeta : metav1 . ObjectMeta {
Name : "dashboard-1" ,
} ,
mockItems : [ ] unstructured . Unstructured {
{
Object : map [ string ] interface { } {
"apiVersion" : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
"kind" : "Dashboard" ,
"metadata" : map [ string ] interface { } {
"name" : "dashboard-1" ,
} ,
} ,
} , nil
} ,
} ,
expectedError : "export dashboards: too many errors encountered" ,
setupProgress : func ( progress * jobs . MockJobProgressRecorder ) {
@ -205,8 +160,8 @@ func TestExportResources(t *testing.T) {
} ) ) . Return ( )
progress . On ( "TooManyErrors" ) . Return ( fmt . Errorf ( "too many errors encountered" ) )
} ,
setupResources : func ( repoResources * resources . MockRepositoryResources , resourceClients * resources . MockResourceClients , dynamicClient * dynamicfake . FakeDynamicClient , gvk schema . GroupVersionKind ) {
resourceClients . On ( "ForResource" , resources . DashboardResource ) . Return ( dynamicClient . Resource ( resources . DashboardResource ) , gvk , 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" ,
@ -219,24 +174,59 @@ func TestExportResources(t *testing.T) {
} ,
{
name : "ignores existing dashboards" ,
reactorFunc : func ( action k8testing . Action ) ( bool , runtime . Object , error ) {
return true , & metav1 . PartialObjectMetadataList {
TypeMeta : metav1 . TypeMeta {
APIVersion : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
Kind : "DashboardList" ,
mockItems : [ ] unstructured . Unstructured {
{
Object : map [ string ] interface { } {
"apiVersion" : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
"kind" : "Dashboard" ,
"metadata" : map [ string ] interface { } {
"name" : "existing-dashboard" ,
} ,
} ,
Items : [ ] metav1 . PartialObjectMetadata {
{
TypeMeta : metav1 . TypeMeta {
APIVersion : resources . DashboardResource . GroupVersion ( ) . String ( ) ,
Kind : "Dashboard" ,
} ,
ObjectMeta : metav1 . ObjectMeta {
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" ,
} ,
} ,
} ,
} , nil
} ,
} ,
expectedError : "" ,
setupProgress : func ( progress * jobs . MockJobProgressRecorder ) {
@ -247,50 +237,195 @@ func TestExportResources(t *testing.T) {
} ) ) . Return ( )
progress . On ( "TooManyErrors" ) . Return ( nil )
} ,
setupResources : func ( repoResources * resources . MockRepositoryResources , resourceClients * resources . MockResourceClients , dynamicClient * dynamicfake . FakeDynamicClient , gvk schema . GroupVersionKind ) {
resourceClients . On ( "ForResource" , resources . DashboardResource ) . Return ( dynamicClient . Resource ( resources . DashboardResource ) , gvk , 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" ,
}
// Return true to indicate the file already exists, and provide the updated path
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 ( "" , resources . ErrAlreadyInRepository )
} ) , 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 ) {
scheme := runtime . NewScheme ( )
require . NoError ( t , metav1 . AddMetaToScheme ( scheme ) )
listGVK := schema . GroupVersionKind {
Group : resources . DashboardResource . Group ,
Version : resources . DashboardResource . Version ,
Kind : "DashboardList" ,
mockClient := & mockDynamicInterface {
items : tt . mockItems ,
}
scheme . AddKnownTypeWithName ( listGVK , & metav1 . PartialObjectMetadataList { } )
scheme . AddKnownTypeWithName ( schema . GroupVersionKind {
Group : resources . DashboardResource . Group ,
Version : resources . DashboardResource . Version ,
Kind : resources . DashboardResource . Resource ,
} , & metav1 . PartialObjectMetadata { } )
fakeDynamicClient := dynamicfake . NewSimpleDynamicClientWithCustomListKinds ( scheme , map [ schema . GroupVersionResource ] string {
resources . DashboardResource : listGVK . Kind ,
} )
resourceClients := resources . NewMockResourceClients ( t )
fakeDynamicClient . PrependReactor ( "list" , "dashboards" , tt . reactorFunc )
mockProgress := jobs . NewMockJobProgressRecorder ( t )
tt . setupProgress ( mockProgress )
repoResources := resources . NewMockRepositoryResources ( t )
tt . setupResources ( repoResources , resourceClients , fakeDynamicClient , listGVK )
tt . setupResources ( repoResources , resourceClients , mockClient , schema . GroupVersionKind {
Group : resources . DashboardResource . Group ,
Version : resources . DashboardResource . Version ,
Kind : "DashboardList" ,
} )
options := provisioningV0 . ExportJobOptions {
Path : "grafana" ,