mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 07:21:50 +08:00

* 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
915 lines
31 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|