Files
Roberto Jiménez Sánchez 047499a363 Provisioning: introduce concept of provisioning extras (#104981)
* Spike: Extras

* Attempt to wire it up

* Hack

* Fix issue with jobs

* Wire more things up

* Fix more wiring stuff

* Remove webhook secret key from main registration

* Move secret encryption also outside register

* Add TODOs in code

* Add more explanations

* Move connectors to different package

* Move pull request job into webhooks

* Separate registration

* Remove duplicate files

* Fix missing function

* Extract webhook repository logic out of the core github repository

* Use status patcher in webhook connector

* Fix change in go mod

* Change hooks signature

* Remove TODOs

* Remove Webhook methos from go-git

* Remove leftover

* Fix mistake in OpenAPI spec

* Fix some tests

* Fix some issues

* Fix linting
2025-05-13 09:50:43 +02:00

915 lines
31 KiB
Go

package pullrequest
import (
"context"
"crypto/sha256"
"encoding/binary"
"fmt"
"testing"
"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/schema"
"github.com/grafana/grafana/pkg/apimachinery/utils"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
)
func TestCalculateChanges(t *testing.T) {
tests := []struct {
name string
setupMocks func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory)
changes []repository.VersionedFileChange
expectedInfo changeInfo
expectedError string
grafanaBaseURL string
}{
{
name: "with screenshot",
setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) {
finfo := &repository.FileInfo{
Path: "path/to/file.json",
Ref: "ref",
Data: []byte("xxxx"),
}
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resources.DashboardResource.GroupVersion().String(),
"kind": dashboardKind,
"metadata": map[string]interface{}{
"name": "the-uid",
},
"spec": map[string]interface{}{
"title": "hello world",
},
},
}
meta, _ := utils.MetaAccessor(obj)
progress.On("SetMessage", mock.Anything, "process path/to/file.json").Return()
reader.On("Read", mock.Anything, "path/to/file.json", "ref").Return(finfo, nil)
reader.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "x",
},
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
GenerateDashboardPreviews: true,
},
},
})
parser.On("Parse", mock.Anything, finfo).Return(&resources.ParsedResource{
Info: finfo,
Repo: provisioning.ResourceRepositoryInfo{
Namespace: "x",
Name: "y",
},
GVK: schema.GroupVersionKind{
Kind: dashboardKind,
},
Obj: obj,
Existing: obj,
Meta: meta,
DryRunResponse: obj,
}, nil)
renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(true)
renderer.On("RenderScreenshot", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(getDummyRenderedURL("x"), nil)
parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil)
},
changes: []repository.VersionedFileChange{{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
}},
expectedInfo: changeInfo{
Changes: []fileChangeInfo{{
Change: repository.VersionedFileChange{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
},
GrafanaURL: "http://host/d/the-uid/hello-world",
PreviewURL: "http://host/admin/provisioning/y/dashboard/preview/path/to/file.json?pull_request_url=http%253A%252F%252Fgithub.com%252Fpr%252F&ref=ref",
GrafanaScreenshotURL: "https://cdn2.thecatapi.com/images/9e2.jpg",
PreviewScreenshotURL: "https://cdn2.thecatapi.com/images/9e2.jpg",
}},
},
},
{
name: "without screenshot",
setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) {
finfo := &repository.FileInfo{
Path: "path/to/file.json",
Ref: "ref",
Data: []byte("xxxx"),
}
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resources.DashboardResource.GroupVersion().String(),
"kind": dashboardKind,
"metadata": map[string]interface{}{
"name": "the-uid",
},
"spec": map[string]interface{}{
"title": "hello world",
},
},
}
meta, _ := utils.MetaAccessor(obj)
progress.On("SetMessage", mock.Anything, "process path/to/file.json").Return()
reader.On("Read", mock.Anything, "path/to/file.json", "ref").Return(finfo, nil)
reader.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "x",
},
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
GenerateDashboardPreviews: true,
},
},
})
parser.On("Parse", mock.Anything, finfo).Return(&resources.ParsedResource{
Info: finfo,
Repo: provisioning.ResourceRepositoryInfo{
Namespace: "x",
Name: "y",
},
GVK: schema.GroupVersionKind{
Kind: dashboardKind,
},
Obj: obj,
Existing: obj,
Meta: meta,
DryRunResponse: obj,
}, nil)
renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(false)
parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil)
},
changes: []repository.VersionedFileChange{{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
}},
expectedInfo: changeInfo{
Changes: []fileChangeInfo{{
Change: repository.VersionedFileChange{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
},
GrafanaURL: "http://host/d/the-uid/hello-world",
PreviewURL: "http://host/admin/provisioning/y/dashboard/preview/path/to/file.json?pull_request_url=http%253A%252F%252Fgithub.com%252Fpr%252F&ref=ref",
GrafanaScreenshotURL: "",
PreviewScreenshotURL: "",
}},
},
},
{
name: "process first 10 files",
setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) {
finfo := &repository.FileInfo{
Path: "path/to/file.json",
Ref: "ref",
Data: []byte("xxxx"),
}
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resources.DashboardResource.GroupVersion().String(),
"kind": dashboardKind,
"metadata": map[string]interface{}{
"name": "the-uid",
},
"spec": map[string]interface{}{
"title": "hello world",
},
},
}
meta, _ := utils.MetaAccessor(obj)
progress.On("SetMessage", mock.Anything, mock.Anything).Return()
reader.On("Read", mock.Anything, "path/to/file.json", "ref").Return(finfo, nil)
reader.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "x",
},
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
GenerateDashboardPreviews: true,
},
},
})
parser.On("Parse", mock.Anything, finfo).Return(&resources.ParsedResource{
Info: finfo,
Repo: provisioning.ResourceRepositoryInfo{
Namespace: "x",
Name: "y",
},
GVK: schema.GroupVersionKind{
Kind: dashboardKind,
},
Obj: obj,
Existing: obj,
Meta: meta,
DryRunResponse: obj,
}, nil)
renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(true)
parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil)
},
changes: func() []repository.VersionedFileChange {
changes := []repository.VersionedFileChange{}
for range 15 {
changes = append(changes, repository.VersionedFileChange{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
})
}
return changes
}(),
expectedInfo: changeInfo{
SkippedFiles: 5,
Changes: func() []fileChangeInfo {
changes := []fileChangeInfo{}
for range 10 {
changes = append(changes, fileChangeInfo{
Change: repository.VersionedFileChange{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
},
GrafanaURL: "http://host/d/the-uid/hello-world",
PreviewURL: "http://host/admin/provisioning/y/dashboard/preview/path/to/file.json?pull_request_url=http%253A%252F%252Fgithub.com%252Fpr%252F&ref=ref",
})
}
return changes
}(),
},
},
{
name: "parser factory error",
setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) {
reader.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "x",
},
})
parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("parser factory error"))
},
changes: []repository.VersionedFileChange{{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
}},
expectedError: "failed to get parser for test-repo: parser factory error",
},
{
name: "file read error",
setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) {
reader.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "x",
},
})
renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(false)
parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil)
progress.On("SetMessage", mock.Anything, "process path/to/file.json").Return()
reader.On("Read", mock.Anything, "path/to/file.json", "ref").Return(nil, fmt.Errorf("read error"))
},
changes: []repository.VersionedFileChange{{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
}},
expectedInfo: changeInfo{
Changes: []fileChangeInfo{{
Change: repository.VersionedFileChange{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
},
Error: "read error",
}},
},
},
{
name: "parse error",
setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) {
reader.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "x",
},
})
parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil)
renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(false)
progress.On("SetMessage", mock.Anything, "process path/to/file.json").Return()
finfo := &repository.FileInfo{
Path: "path/to/file.json",
Ref: "ref",
Data: []byte("invalid json"),
}
reader.On("Read", mock.Anything, "path/to/file.json", "ref").Return(finfo, nil)
parser.On("Parse", mock.Anything, finfo).Return(nil, fmt.Errorf("parse error"))
},
changes: []repository.VersionedFileChange{{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
}},
expectedInfo: changeInfo{
Changes: []fileChangeInfo{{
Change: repository.VersionedFileChange{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
},
Error: "parse error",
}},
},
},
{
name: "dry run error",
setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) {
reader.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "x",
},
})
parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil)
progress.On("SetMessage", mock.Anything, "process path/to/file.json").Return()
renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(false)
finfo := &repository.FileInfo{
Path: "path/to/file.json",
Ref: "ref",
Data: []byte("xxxx"),
}
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resources.DashboardResource.GroupVersion().String(),
"kind": dashboardKind,
"metadata": map[string]interface{}{
"name": "the-uid",
},
"spec": map[string]interface{}{
"title": "hello world",
},
},
}
meta, _ := utils.MetaAccessor(obj)
reader.On("Read", mock.Anything, "path/to/file.json", "ref").Return(finfo, nil)
parsed := &resources.ParsedResource{
Info: finfo,
Repo: provisioning.ResourceRepositoryInfo{
Namespace: "x",
Name: "y",
},
GVK: schema.GroupVersionKind{
Kind: dashboardKind,
},
Obj: obj,
Existing: obj,
Meta: meta,
}
parser.On("Parse", mock.Anything, finfo).Return(parsed, nil)
parsed.DryRunResponse = nil // This will cause a dry run error
},
changes: []repository.VersionedFileChange{{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
}},
expectedInfo: changeInfo{
Changes: []fileChangeInfo{{
Change: repository.VersionedFileChange{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
},
Error: "no client configured",
Title: "hello world",
Parsed: &resources.ParsedResource{
Info: &repository.FileInfo{
Path: "path/to/file.json",
Ref: "ref",
Data: []byte("xxxx"),
},
GVK: schema.GroupVersionKind{
Kind: dashboardKind,
},
},
}},
},
},
{
name: "screenshot render error",
setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) {
finfo := &repository.FileInfo{
Path: "path/to/file.json",
Ref: "ref",
Data: []byte("xxxx"),
}
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resources.DashboardResource.GroupVersion().String(),
"kind": dashboardKind,
"metadata": map[string]interface{}{
"name": "the-uid",
},
"spec": map[string]interface{}{
"title": "hello world",
},
},
}
meta, _ := utils.MetaAccessor(obj)
renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(true)
progress.On("SetMessage", mock.Anything, "process path/to/file.json").Return()
reader.On("Read", mock.Anything, "path/to/file.json", "ref").Return(finfo, nil)
reader.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "x",
},
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
GenerateDashboardPreviews: true,
},
},
})
parser.On("Parse", mock.Anything, finfo).Return(&resources.ParsedResource{
Info: finfo,
Repo: provisioning.ResourceRepositoryInfo{
Namespace: "x",
Name: "y",
},
GVK: schema.GroupVersionKind{
Kind: dashboardKind,
},
Obj: obj,
Existing: obj,
Meta: meta,
DryRunResponse: obj,
}, nil)
renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(true)
renderer.On("RenderScreenshot", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return("", fmt.Errorf("render error"))
parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil)
},
changes: []repository.VersionedFileChange{{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
}},
expectedInfo: changeInfo{
MissingImageRenderer: true,
Changes: []fileChangeInfo{{
Change: repository.VersionedFileChange{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
},
Error: "error rendering screenshot: render error",
GrafanaURL: "http://host/d/the-uid/hello-world",
PreviewURL: "http://host/admin/provisioning/y/dashboard/preview/path/to/file.json?pull_request_url=http%253A%252F%252Fgithub.com%252Fpr%252F&ref=ref",
}},
},
},
{
name: "non-dashboard resource",
setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) {
finfo := &repository.FileInfo{
Path: "path/to/file.json",
Ref: "ref",
Data: []byte("xxxx"),
}
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "test/v1",
"kind": "TestResource",
"metadata": map[string]interface{}{
"name": "test-resource",
},
"spec": map[string]interface{}{
"title": "Test Resource",
},
},
}
meta, _ := utils.MetaAccessor(obj)
renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(false)
progress.On("SetMessage", mock.Anything, "process path/to/file.json").Return()
reader.On("Read", mock.Anything, "path/to/file.json", "ref").Return(finfo, nil)
reader.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "x",
},
})
parser.On("Parse", mock.Anything, finfo).Return(&resources.ParsedResource{
Info: finfo,
Repo: provisioning.ResourceRepositoryInfo{
Namespace: "x",
Name: "y",
},
GVK: schema.GroupVersionKind{
Kind: "TestResource",
},
Obj: obj,
Existing: obj,
Meta: meta,
DryRunResponse: obj,
}, nil)
parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil)
},
changes: []repository.VersionedFileChange{{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
}},
expectedInfo: changeInfo{
Changes: []fileChangeInfo{{
Change: repository.VersionedFileChange{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
},
Title: "Test Resource",
Parsed: &resources.ParsedResource{
Info: &repository.FileInfo{
Path: "path/to/file.json",
Ref: "ref",
Data: []byte("xxxx"),
},
GVK: schema.GroupVersionKind{
Kind: "TestResource",
},
},
}},
},
},
{
name: "deleted file",
setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) {
reader.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "x",
},
})
renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(false)
parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil)
progress.On("SetMessage", mock.Anything, "process path/to/file.json").Return()
},
changes: []repository.VersionedFileChange{{
Action: repository.FileActionDeleted,
Path: "path/to/file.json",
Ref: "ref",
}},
expectedInfo: changeInfo{
Changes: []fileChangeInfo{{
Change: repository.VersionedFileChange{
Action: repository.FileActionDeleted,
Path: "path/to/file.json",
Ref: "ref",
},
Error: "delete feedback not yet implemented",
}},
},
},
{
name: "invalid grafana url",
setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) {
finfo := &repository.FileInfo{
Path: "path/to/file.json",
Ref: "ref",
Data: []byte("xxxx"),
}
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resources.DashboardResource.GroupVersion().String(),
"kind": dashboardKind,
"metadata": map[string]interface{}{
"name": "the:uid", // Invalid character in UID
},
"spec": map[string]interface{}{
"title": "hello world",
},
},
}
meta, _ := utils.MetaAccessor(obj)
renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(true)
renderer.On("RenderScreenshot", mock.Anything, mock.MatchedBy(func(repo provisioning.ResourceRepositoryInfo) bool {
return repo.Namespace == "x" && repo.Name == "y"
}), "d/the:uid/hello-world", mock.Anything).Return("", fmt.Errorf("invalid URL"))
renderer.On("RenderScreenshot", mock.Anything, mock.MatchedBy(func(repo provisioning.ResourceRepositoryInfo) bool {
return repo.Namespace == "x" && repo.Name == "y"
}), "admin/provisioning/y/dashboard/preview/path/to/file.json", mock.Anything).Return("", fmt.Errorf("invalid preview URL"))
progress.On("SetMessage", mock.Anything, "process path/to/file.json").Return()
reader.On("Read", mock.Anything, "path/to/file.json", "ref").Return(finfo, nil)
reader.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "x",
},
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
GenerateDashboardPreviews: true,
},
},
})
parser.On("Parse", mock.Anything, finfo).Return(&resources.ParsedResource{
Info: finfo,
Repo: provisioning.ResourceRepositoryInfo{
Namespace: "x",
Name: "y",
},
GVK: schema.GroupVersionKind{
Kind: dashboardKind,
},
Obj: obj,
Existing: obj,
Meta: meta,
DryRunResponse: obj,
}, nil)
parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil)
},
changes: []repository.VersionedFileChange{{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
}},
expectedInfo: changeInfo{
MissingImageRenderer: true,
Changes: []fileChangeInfo{{
Change: repository.VersionedFileChange{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
},
Error: "error rendering screenshot: invalid preview URL",
GrafanaURL: "http://host/d/the:uid/hello-world", // Invalid URL
PreviewURL: "http://host/admin/provisioning/y/dashboard/preview/path/to/file.json?pull_request_url=http%253A%252F%252Fgithub.com%252Fpr%252F&ref=ref",
}},
},
},
{
name: "malformed grafana url",
grafanaBaseURL: "ht tp://bad url/",
setupMocks: func(parser *resources.MockParser, reader *repository.MockReader, progress *jobs.MockJobProgressRecorder, renderer *MockScreenshotRenderer, parserFactory *resources.MockParserFactory) {
finfo := &repository.FileInfo{
Path: "path/to/file.json",
Ref: "ref",
Data: []byte("xxxx"),
}
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resources.DashboardResource.GroupVersion().String(),
"kind": dashboardKind,
"metadata": map[string]interface{}{
"name": "the-uid",
},
"spec": map[string]interface{}{
"title": "hello world",
},
},
}
meta, _ := utils.MetaAccessor(obj)
renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(false)
progress.On("SetMessage", mock.Anything, "process path/to/file.json").Return()
reader.On("Read", mock.Anything, "path/to/file.json", "ref").Return(finfo, nil)
reader.On("Config").Return(&provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
Namespace: "x",
},
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
GenerateDashboardPreviews: true,
},
},
})
parsed := &resources.ParsedResource{
Info: finfo,
Repo: provisioning.ResourceRepositoryInfo{
Namespace: "x",
Name: "y",
},
GVK: schema.GroupVersionKind{
Kind: dashboardKind,
},
Obj: obj,
Existing: obj,
Meta: meta,
DryRunResponse: obj,
}
parser.On("Parse", mock.Anything, finfo).Return(parsed, nil)
parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil)
},
changes: []repository.VersionedFileChange{{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
}},
expectedInfo: changeInfo{
MissingImageRenderer: true,
Changes: []fileChangeInfo{{
Change: repository.VersionedFileChange{
Action: repository.FileActionCreated,
Path: "path/to/file.json",
Ref: "ref",
},
GrafanaURL: "ht tp://bad url/d/the-uid/hello-world", // Malformed URL
PreviewURL: "ht tp://bad url/admin/provisioning/y/dashboard/preview/path/to/file.json?pull_request_url=http%253A%252F%252Fgithub.com%252Fpr%252F&ref=ref",
}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := resources.NewMockParser(t)
reader := repository.NewMockReader(t)
progress := jobs.NewMockJobProgressRecorder(t)
renderer := NewMockScreenshotRenderer(t)
parserFactory := resources.NewMockParserFactory(t)
tt.setupMocks(parser, reader, progress, renderer, parserFactory)
evaluator := NewEvaluator(renderer, parserFactory, func(_ string) string {
if tt.grafanaBaseURL != "" {
return tt.grafanaBaseURL
}
return "http://host/"
})
pullRequest := provisioning.PullRequestJobOptions{
Ref: "ref",
PR: 123,
URL: "http://github.com/pr/",
}
info, err := evaluator.Evaluate(context.Background(), reader, pullRequest, tt.changes, progress)
if tt.expectedError != "" {
require.EqualError(t, err, tt.expectedError)
return
}
require.NoError(t, err)
require.Equal(t, len(tt.expectedInfo.Changes), len(info.Changes))
require.Equal(t, tt.expectedInfo.SkippedFiles, info.SkippedFiles)
// compare change URLs
for i, change := range info.Changes {
require.Equal(t, tt.expectedInfo.Changes[i].GrafanaURL, change.GrafanaURL)
require.Equal(t, tt.expectedInfo.Changes[i].PreviewURL, change.PreviewURL)
require.Equal(t, tt.expectedInfo.Changes[i].GrafanaScreenshotURL, change.GrafanaScreenshotURL)
require.Equal(t, tt.expectedInfo.Changes[i].PreviewScreenshotURL, change.PreviewScreenshotURL)
require.Equal(t, tt.expectedInfo.Changes[i].Error, change.Error)
}
})
}
}
func TestDummyImageURL(t *testing.T) {
urls := []string{}
for i := range 10 {
urls = append(urls, getDummyRenderedURL(fmt.Sprintf("http://%d", i)))
}
require.Equal(t, []string{
"https://cdn2.thecatapi.com/images/9e2.jpg",
"https://cdn2.thecatapi.com/images/bhs.jpg",
"https://cdn2.thecatapi.com/images/d54.jpg",
"https://cdn2.thecatapi.com/images/99c.jpg",
"https://cdn2.thecatapi.com/images/9e2.jpg",
"https://cdn2.thecatapi.com/images/bhs.jpg",
"https://cdn2.thecatapi.com/images/d54.jpg",
"https://cdn2.thecatapi.com/images/99c.jpg",
"https://cdn2.thecatapi.com/images/9e2.jpg",
"https://cdn2.thecatapi.com/images/bhs.jpg",
}, urls)
}
// Returns a random (but stable) image for a string
func getDummyRenderedURL(url string) string {
dummy := []string{
"https://cdn2.thecatapi.com/images/9e2.jpg",
"https://cdn2.thecatapi.com/images/bhs.jpg",
"https://cdn2.thecatapi.com/images/d54.jpg",
"https://cdn2.thecatapi.com/images/99c.jpg",
}
idx := 0
hash := sha256.New()
bytes := hash.Sum([]byte(url))
if len(bytes) > 8 {
v := binary.BigEndian.Uint64(bytes[0:8])
idx = int(v) % len(dummy)
}
return dummy[idx]
}
// FIXME: test these cases from the public interface once the component is refactored
func TestRenderScreenshotFromGrafanaURL(t *testing.T) {
tests := []struct {
name string
baseURL string
grafanaURL string
setupMock func(renderer *MockScreenshotRenderer)
wantSnap string
wantErr string
}{
{
name: "invalid grafana url",
baseURL: "http://host/",
grafanaURL: "ht tp://host/d/uid/dashboard",
setupMock: func(renderer *MockScreenshotRenderer) {},
wantErr: `parse "ht tp://host/d/uid/dashboard": first path segment in URL cannot contain colon`,
},
{
name: "invalid base url",
baseURL: "ht tp://bad host/",
grafanaURL: "http://host/d/uid/dashboard",
setupMock: func(renderer *MockScreenshotRenderer) {
renderer.On("RenderScreenshot", mock.Anything, mock.MatchedBy(func(repo provisioning.ResourceRepositoryInfo) bool {
return repo.Namespace == "test" && repo.Name == "repo"
}), "d/uid/dashboard", mock.Anything).Return("screenshot.png", nil)
},
wantErr: `parse "ht tp://bad host/": first path segment in URL cannot contain colon`,
},
{
name: "render error",
baseURL: "http://host/",
grafanaURL: "http://host/d/uid/dashboard",
setupMock: func(renderer *MockScreenshotRenderer) {
renderer.On("RenderScreenshot", mock.Anything, mock.MatchedBy(func(repo provisioning.ResourceRepositoryInfo) bool {
return repo.Namespace == "test" && repo.Name == "repo"
}), "d/uid/dashboard", mock.Anything).Return("", fmt.Errorf("render failed"))
},
wantErr: "error rendering screenshot: render failed",
},
{
name: "cdn url returned",
baseURL: "http://host/",
grafanaURL: "http://host/d/uid/dashboard",
setupMock: func(renderer *MockScreenshotRenderer) {
renderer.On("RenderScreenshot", mock.Anything, mock.MatchedBy(func(repo provisioning.ResourceRepositoryInfo) bool {
return repo.Namespace == "test" && repo.Name == "repo"
}), "d/uid/dashboard", mock.Anything).Return("https://cdn.example.com/screenshot.png", nil)
},
wantSnap: "https://cdn.example.com/screenshot.png",
},
{
name: "successful render with relative path",
baseURL: "http://host/",
grafanaURL: "http://host/d/uid/dashboard",
setupMock: func(renderer *MockScreenshotRenderer) {
renderer.On("RenderScreenshot", mock.Anything, mock.MatchedBy(func(repo provisioning.ResourceRepositoryInfo) bool {
return repo.Namespace == "test" && repo.Name == "repo"
}), "d/uid/dashboard", mock.Anything).Return("screenshots/123.png", nil)
},
wantSnap: "http://host/screenshots/123.png",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
renderer := NewMockScreenshotRenderer(t)
tt.setupMock(renderer)
repo := provisioning.ResourceRepositoryInfo{
Namespace: "test",
Name: "repo",
}
got, err := renderScreenshotFromGrafanaURL(context.Background(), tt.baseURL, renderer, repo, tt.grafanaURL)
if tt.wantErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
require.Equal(t, tt.wantSnap, got)
})
}
}