From de9febcab76b1955f19ef733887931e6b4ac3045 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 6 Mar 2025 12:34:14 +0300 Subject: [PATCH] Provisioning: Include URLs in ResourceWrapper response (#101511) --- pkg/apis/provisioning/v0alpha1/jobs.go | 14 +-- pkg/apis/provisioning/v0alpha1/types.go | 36 +++++- .../v0alpha1/zz_generated.deepcopy.go | 46 ++++++- .../v0alpha1/zz_generated.openapi.go | 119 ++++++++++++++++-- ...enerated.openapi_violation_exceptions.list | 1 + .../provisioning/controller/repository.go | 2 +- pkg/registry/apis/provisioning/export.go | 2 +- .../apis/provisioning/jobs/export/worker.go | 2 +- .../apis/provisioning/jobs/migrate/worker.go | 4 +- pkg/registry/apis/provisioning/jobs/store.go | 8 +- .../apis/provisioning/jobs/sync/worker.go | 2 +- .../apis/provisioning/repository/github.go | 40 +++++- .../provisioning/repository/github_test.go | 4 +- .../provisioning/repository/repository.go | 8 ++ .../provisioning/resources/fileformat_test.go | 7 +- .../apis/provisioning/resources/parser.go | 52 ++++++-- pkg/registry/apis/provisioning/sync.go | 2 +- .../provisioning.grafana.app-v0alpha1.json | 112 ++++++++++++++--- .../provisioned/DashboardPreviewBanner.tsx | 14 +-- .../provisioning/api/endpoints.gen.ts | 66 +++++++--- 20 files changed, 435 insertions(+), 106 deletions(-) diff --git a/pkg/apis/provisioning/v0alpha1/jobs.go b/pkg/apis/provisioning/v0alpha1/jobs.go index f9d19ffa6cc..d621bf3411f 100644 --- a/pkg/apis/provisioning/v0alpha1/jobs.go +++ b/pkg/apis/provisioning/v0alpha1/jobs.go @@ -26,15 +26,15 @@ type JobList struct { type JobAction string const ( - // Update a pull request -- send preview images, links etc - JobActionPullRequest JobAction = "pr" - // Sync the remote branch with the grafana instance JobActionSync JobAction = "pull" // Export from grafana into the remote repository JobActionExport JobAction = "push" + // Process a pull request -- apply comments with preview images, links etc + JobActionPullRequest JobAction = "pr" + // Migration task -- this will migrate an full instance from SQL > Git JobActionMigrate JobAction = "migrate" ) @@ -69,11 +69,11 @@ type JobSpec struct { // Pull request options PullRequest *PullRequestJobOptions `json:"pr,omitempty"` - // Required when the action is `export` - Export *ExportJobOptions `json:"export,omitempty"` + // Required when the action is `push` + Push *ExportJobOptions `json:"push,omitempty"` - // Required when the action is `sync` - Sync *SyncJobOptions `json:"sync,omitempty"` + // Required when the action is `pull` + Pull *SyncJobOptions `json:"pull,omitempty"` // Required when the action is `migrate` Migrate *MigrateJobOptions `json:"migrate,omitempty"` diff --git a/pkg/apis/provisioning/v0alpha1/types.go b/pkg/apis/provisioning/v0alpha1/types.go index c974eef45b9..32947620679 100644 --- a/pkg/apis/provisioning/v0alpha1/types.go +++ b/pkg/apis/provisioning/v0alpha1/types.go @@ -219,12 +219,18 @@ type ResourceWrapper struct { // Path to the remote file Path string `json:"path,omitempty"` - // The commit hash (if exists) + // The request ref (or branch if exists) Ref string `json:"ref,omitempty"` // The repo hash value Hash string `json:"hash,omitempty"` + // Basic repository info + Repository ResourceRepositoryInfo `json:"repository"` + + // Typed links for this file (only supported by external systems, github etc) + URLs *ResourceURLs `json:"urls,omitempty"` + // The modified time in the remote file system Timestamp *metav1.Time `json:"timestamp,omitempty"` @@ -264,6 +270,34 @@ type ResourceObjects struct { DryRun common.Unstructured `json:"dryRun,omitempty"` } +type ResourceRepositoryInfo struct { + // The repository type + Type RepositoryType `json:"type"` + + // The display name for this repository + Title string `json:"title"` + + // The namespace this belongs to + Namespace string `json:"namespace"` + + // The name (identifier) + Name string `json:"name"` +} + +type ResourceURLs struct { + // A URL pointing to the this file in the repository + SourceURL string `json:"sourceURL,omitempty"` + + // A URL pointing to the repository this lives in + RepositoryURL string `json:"repositoryURL,omitempty"` + + // A URL that will create a new pull requeset for this branch + NewPullRequestURL string `json:"newPullRequestURL,omitempty"` + + // Compare this version to the target branch + CompareURL string `json:"compareURL,omitempty"` +} + // Information we can get just from the file listing // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type FileList struct { diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go index e5d06e17b8e..2d0da3b4dbd 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go @@ -276,13 +276,13 @@ func (in *JobSpec) DeepCopyInto(out *JobSpec) { *out = new(PullRequestJobOptions) **out = **in } - if in.Export != nil { - in, out := &in.Export, &out.Export + if in.Push != nil { + in, out := &in.Push, &out.Push *out = new(ExportJobOptions) **out = **in } - if in.Sync != nil { - in, out := &in.Sync, &out.Sync + if in.Pull != nil { + in, out := &in.Pull, &out.Pull *out = new(SyncJobOptions) **out = **in } @@ -634,6 +634,22 @@ func (in *ResourceObjects) DeepCopy() *ResourceObjects { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceRepositoryInfo) DeepCopyInto(out *ResourceRepositoryInfo) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceRepositoryInfo. +func (in *ResourceRepositoryInfo) DeepCopy() *ResourceRepositoryInfo { + if in == nil { + return nil + } + out := new(ResourceRepositoryInfo) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResourceStats) DeepCopyInto(out *ResourceStats) { *out = *in @@ -681,10 +697,32 @@ func (in *ResourceType) DeepCopy() *ResourceType { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceURLs) DeepCopyInto(out *ResourceURLs) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceURLs. +func (in *ResourceURLs) DeepCopy() *ResourceURLs { + if in == nil { + return nil + } + out := new(ResourceURLs) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResourceWrapper) DeepCopyInto(out *ResourceWrapper) { *out = *in out.TypeMeta = in.TypeMeta + out.Repository = in.Repository + if in.URLs != nil { + in, out := &in.URLs, &out.URLs + *out = new(ResourceURLs) + **out = **in + } if in.Timestamp != nil { in, out := &in.Timestamp, &out.Timestamp *out = (*in).DeepCopy() diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index 52db64f638f..d552dc783dc 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -40,8 +40,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceList": schema_pkg_apis_provisioning_v0alpha1_ResourceList(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceListItem": schema_pkg_apis_provisioning_v0alpha1_ResourceListItem(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceObjects": schema_pkg_apis_provisioning_v0alpha1_ResourceObjects(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceRepositoryInfo": schema_pkg_apis_provisioning_v0alpha1_ResourceRepositoryInfo(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceStats": schema_pkg_apis_provisioning_v0alpha1_ResourceStats(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceType": schema_pkg_apis_provisioning_v0alpha1_ResourceType(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceURLs": schema_pkg_apis_provisioning_v0alpha1_ResourceURLs(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceWrapper": schema_pkg_apis_provisioning_v0alpha1_ResourceWrapper(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.SyncJobOptions": schema_pkg_apis_provisioning_v0alpha1_SyncJobOptions(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.SyncOptions": schema_pkg_apis_provisioning_v0alpha1_SyncOptions(ref), @@ -618,11 +620,11 @@ func schema_pkg_apis_provisioning_v0alpha1_JobSpec(ref common.ReferenceCallback) Properties: map[string]spec.Schema{ "action": { SchemaProps: spec.SchemaProps{ - Description: "Possible enum values:\n - `\"export\"` Export from grafana into the remote repository\n - `\"migrate\"` Migration task -- this will migrate an full instance from SQL > Git\n - `\"pr\"` Update a pull request -- send preview images, links etc\n - `\"sync\"` Sync the remote branch with the grafana instance", + Description: "Possible enum values:\n - `\"migrate\"` Migration task -- this will migrate an full instance from SQL > Git\n - `\"pr\"` Process a pull request -- apply comments with preview images, links etc\n - `\"pull\"` Sync the remote branch with the grafana instance\n - `\"push\"` Export from grafana into the remote repository", Default: "", Type: []string{"string"}, Format: "", - Enum: []interface{}{"export", "migrate", "pr", "sync"}, + Enum: []interface{}{"migrate", "pr", "pull", "push"}, }, }, "repository": { @@ -639,15 +641,15 @@ func schema_pkg_apis_provisioning_v0alpha1_JobSpec(ref common.ReferenceCallback) Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.PullRequestJobOptions"), }, }, - "export": { + "push": { SchemaProps: spec.SchemaProps{ - Description: "Required when the action is `export`", + Description: "Required when the action is `push`", Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ExportJobOptions"), }, }, - "sync": { + "pull": { SchemaProps: spec.SchemaProps{ - Description: "Required when the action is `sync`", + Description: "Required when the action is `pull`", Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.SyncJobOptions"), }, }, @@ -1387,6 +1389,52 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceObjects(ref common.ReferenceC } } +func schema_pkg_apis_provisioning_v0alpha1_ResourceRepositoryInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Description: "The repository type\n\nPossible enum values:\n - `\"github\"`\n - `\"local\"`", + Default: "", + Type: []string{"string"}, + Format: "", + Enum: []interface{}{"github", "local"}, + }, + }, + "title": { + SchemaProps: spec.SchemaProps{ + Description: "The display name for this repository", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "The namespace this belongs to", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Description: "The name (identifier)", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"type", "title", "namespace", "name"}, + }, + }, + } +} + func schema_pkg_apis_provisioning_v0alpha1_ResourceStats(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1484,6 +1532,46 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceType(ref common.ReferenceCall } } +func schema_pkg_apis_provisioning_v0alpha1_ResourceURLs(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "sourceURL": { + SchemaProps: spec.SchemaProps{ + Description: "A URL pointing to the this file in the repository", + Type: []string{"string"}, + Format: "", + }, + }, + "repositoryURL": { + SchemaProps: spec.SchemaProps{ + Description: "A URL pointing to the repository this lives in", + Type: []string{"string"}, + Format: "", + }, + }, + "newPullRequestURL": { + SchemaProps: spec.SchemaProps{ + Description: "A URL that will create a new pull requeset for this branch", + Type: []string{"string"}, + Format: "", + }, + }, + "compareURL": { + SchemaProps: spec.SchemaProps{ + Description: "Compare this version to the target branch", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + func schema_pkg_apis_provisioning_v0alpha1_ResourceWrapper(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1514,7 +1602,7 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceWrapper(ref common.ReferenceC }, "ref": { SchemaProps: spec.SchemaProps{ - Description: "The commit hash (if exists)", + Description: "The request ref (or branch if exists)", Type: []string{"string"}, Format: "", }, @@ -1526,6 +1614,19 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceWrapper(ref common.ReferenceC Format: "", }, }, + "repository": { + SchemaProps: spec.SchemaProps{ + Description: "Basic repository info", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceRepositoryInfo"), + }, + }, + "urls": { + SchemaProps: spec.SchemaProps{ + Description: "Typed links for this file (only supported by external systems, github etc)", + Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceURLs"), + }, + }, "timestamp": { SchemaProps: spec.SchemaProps{ Description: "The modified time in the remote file system", @@ -1560,11 +1661,11 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceWrapper(ref common.ReferenceC }, }, }, - Required: []string{"resource"}, + Required: []string{"repository", "resource"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceObjects", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceObjects", "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceRepositoryInfo", "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceURLs", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, } } diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list index 696b361ec80..95d45ccebb1 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -12,5 +12,6 @@ API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provis API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,WebhookStatus,SubscribedEvents API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,JobSpec,PullRequest API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositorySpec,GitHub +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ResourceWrapper,URLs API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,SyncStatus,JobID API rule violation: names_match,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,WebhookResponse,Message diff --git a/pkg/registry/apis/provisioning/controller/repository.go b/pkg/registry/apis/provisioning/controller/repository.go index a3ade5e1468..3cbf08b4633 100644 --- a/pkg/registry/apis/provisioning/controller/repository.go +++ b/pkg/registry/apis/provisioning/controller/repository.go @@ -366,7 +366,7 @@ func (rc *RepositoryController) addSyncJob(ctx context.Context, obj *provisionin Spec: provisioning.JobSpec{ Repository: obj.GetName(), Action: provisioning.JobActionSync, - Sync: syncOptions, + Pull: syncOptions, }, }) if err != nil { diff --git a/pkg/registry/apis/provisioning/export.go b/pkg/registry/apis/provisioning/export.go index df108b0eedb..9b3323d8934 100644 --- a/pkg/registry/apis/provisioning/export.go +++ b/pkg/registry/apis/provisioning/export.go @@ -71,7 +71,7 @@ func (c *exportConnector) Connect( Spec: provisioning.JobSpec{ Action: provisioning.JobActionExport, Repository: cfg.Name, - Export: options, + Push: options, }, }) if err != nil { diff --git a/pkg/registry/apis/provisioning/jobs/export/worker.go b/pkg/registry/apis/provisioning/jobs/export/worker.go index 2a53e677fb5..d60580011c6 100644 --- a/pkg/registry/apis/provisioning/jobs/export/worker.go +++ b/pkg/registry/apis/provisioning/jobs/export/worker.go @@ -48,7 +48,7 @@ func (r *ExportWorker) IsSupported(ctx context.Context, job provisioning.Job) bo // Process will start a job func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository, job provisioning.Job, progress jobs.JobProgressRecorder) error { - options := job.Spec.Export + options := job.Spec.Push if options == nil { return errors.New("missing export settings") } diff --git a/pkg/registry/apis/provisioning/jobs/migrate/worker.go b/pkg/registry/apis/provisioning/jobs/migrate/worker.go index 714877e92ae..a3e8da387ee 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/worker.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/worker.go @@ -172,7 +172,7 @@ func (w *MigrationWorker) Process(ctx context.Context, repo repository.Repositor } else { err = w.exportWorker.Process(ctx, rw, provisioning.Job{ Spec: provisioning.JobSpec{ - Export: &provisioning.ExportJobOptions{ + Push: &provisioning.ExportJobOptions{ Identifier: options.Identifier, }, }, @@ -210,7 +210,7 @@ func (w *MigrationWorker) Process(ctx context.Context, repo repository.Repositor // Delegate the import to a sync (from the already checked out go-git repository!) err = w.syncWorker.Process(ctx, rw, provisioning.Job{ Spec: provisioning.JobSpec{ - Sync: &provisioning.SyncJobOptions{ + Pull: &provisioning.SyncJobOptions{ Incremental: false, }, }, diff --git a/pkg/registry/apis/provisioning/jobs/store.go b/pkg/registry/apis/provisioning/jobs/store.go index 89926c8828a..77e13c8c7d4 100644 --- a/pkg/registry/apis/provisioning/jobs/store.go +++ b/pkg/registry/apis/provisioning/jobs/store.go @@ -175,12 +175,12 @@ func (s *jobStore) Add(ctx context.Context, job *provisioning.Job) (*provisionin if job.Spec.Action == "" { return nil, apierrors.NewBadRequest("missing spec.action") } - if job.Spec.Action == provisioning.JobActionExport && job.Spec.Export == nil { - return nil, apierrors.NewBadRequest("missing spec.export") + if job.Spec.Action == provisioning.JobActionExport && job.Spec.Push == nil { + return nil, apierrors.NewBadRequest("missing spec.push") } - if job.Spec.Action == provisioning.JobActionSync && job.Spec.Sync == nil { - return nil, apierrors.NewBadRequest("missing spec.sync") + if job.Spec.Action == provisioning.JobActionSync && job.Spec.Pull == nil { + return nil, apierrors.NewBadRequest("missing spec.pull") } // Only for add diff --git a/pkg/registry/apis/provisioning/jobs/sync/worker.go b/pkg/registry/apis/provisioning/jobs/sync/worker.go index 8ee4f5c8fd0..ff2a7b5df81 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/worker.go +++ b/pkg/registry/apis/provisioning/jobs/sync/worker.go @@ -101,7 +101,7 @@ func (r *SyncWorker) Process(ctx context.Context, repo repository.Repository, jo return fmt.Errorf("failed to create sync job: %w", err) } - syncError := syncJob.run(ctx, *job.Spec.Sync) + syncError := syncJob.run(ctx, *job.Spec.Pull) jobStatus := progress.Complete(ctx, syncError) syncStatus = jobStatus.ToSyncStatus(job.Name) diff --git a/pkg/registry/apis/provisioning/repository/github.go b/pkg/registry/apis/provisioning/repository/github.go index 95186028fee..14e6de31790 100644 --- a/pkg/registry/apis/provisioning/repository/github.go +++ b/pkg/registry/apis/provisioning/repository/github.go @@ -38,11 +38,12 @@ type githubRepository struct { } var ( - _ Repository = (*githubRepository)(nil) - _ Hooks = (*githubRepository)(nil) - _ Versioned = (*githubRepository)(nil) - _ Writer = (*githubRepository)(nil) - _ Reader = (*githubRepository)(nil) + _ Repository = (*githubRepository)(nil) + _ Hooks = (*githubRepository)(nil) + _ Versioned = (*githubRepository)(nil) + _ Writer = (*githubRepository)(nil) + _ Reader = (*githubRepository)(nil) + _ RepositoryWithURLs = (*githubRepository)(nil) ) func NewGitHub( @@ -561,7 +562,7 @@ func (r *githubRepository) parsePushEvent(event *github.PushEvent) (*provisionin Job: &provisioning.JobSpec{ Repository: r.Config().GetName(), Action: provisioning.JobActionSync, - Sync: &provisioning.SyncJobOptions{ + Pull: &provisioning.SyncJobOptions{ Incremental: true, }, }, @@ -709,6 +710,33 @@ func (r *githubRepository) CommentPullRequestFile(ctx context.Context, prNumber return r.gh.CreatePullRequestFileComment(ctx, r.owner, r.repo, prNumber, fileComment) } +// ResourceURLs implements RepositoryWithURLs. +func (r *githubRepository) ResourceURLs(ctx context.Context, file *FileInfo) (*provisioning.ResourceURLs, error) { + cfg := r.config.Spec.GitHub + if file.Path == "" || cfg == nil { + return nil, nil + } + + ref := file.Ref + if ref == "" { + ref = cfg.Branch + } + + urls := &provisioning.ResourceURLs{ + RepositoryURL: cfg.URL, + SourceURL: fmt.Sprintf("%s/blob/%s/%s", cfg.URL, ref, file.Path), + } + + if ref != cfg.Branch { + urls.CompareURL = fmt.Sprintf("%s/compare/%s...%s", cfg.URL, cfg.Branch, ref) + + // Create a new pull request + urls.NewPullRequestURL = fmt.Sprintf("%s?quick_pull=1&labels=grafana", urls.CompareURL) + } + + return urls, nil +} + func (r *githubRepository) createWebhook(ctx context.Context) (pgh.WebhookConfig, error) { secret, err := uuid.NewRandom() if err != nil { diff --git a/pkg/registry/apis/provisioning/repository/github_test.go b/pkg/registry/apis/provisioning/repository/github_test.go index 5e27f11c1c2..dc24f6484e4 100644 --- a/pkg/registry/apis/provisioning/repository/github_test.go +++ b/pkg/registry/apis/provisioning/repository/github_test.go @@ -84,7 +84,7 @@ func TestParseWebhooks(t *testing.T) { Job: &provisioning.JobSpec{ // we want to always push a sync job Repository: "unit-test-repo", Action: provisioning.JobActionSync, - Sync: &provisioning.SyncJobOptions{ + Pull: &provisioning.SyncJobOptions{ Incremental: true, }, }, @@ -94,7 +94,7 @@ func TestParseWebhooks(t *testing.T) { Job: &provisioning.JobSpec{ Repository: "unit-test-repo", Action: provisioning.JobActionSync, - Sync: &provisioning.SyncJobOptions{ + Pull: &provisioning.SyncJobOptions{ Incremental: true, }, }, diff --git a/pkg/registry/apis/provisioning/repository/repository.go b/pkg/registry/apis/provisioning/repository/repository.go index 86f06d4f550..d9d3bc9a4ec 100644 --- a/pkg/registry/apis/provisioning/repository/repository.go +++ b/pkg/registry/apis/provisioning/repository/repository.go @@ -99,6 +99,14 @@ type ReaderWriter interface { Writer } +// Hooks called after the repository has been created, updated or deleted +type RepositoryWithURLs interface { + Repository + + // Get resource URLs for a file inside a repository + ResourceURLs(ctx context.Context, file *FileInfo) (*provisioning.ResourceURLs, error) +} + // Hooks called after the repository has been created, updated or deleted type Hooks interface { Repository diff --git a/pkg/registry/apis/provisioning/resources/fileformat_test.go b/pkg/registry/apis/provisioning/resources/fileformat_test.go index ae09aadff3a..949548b33f4 100644 --- a/pkg/registry/apis/provisioning/resources/fileformat_test.go +++ b/pkg/registry/apis/provisioning/resources/fileformat_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" @@ -97,10 +96,8 @@ spec: require.NoError(t, err) parser := &Parser{ - repo: &provisioning.Repository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - }, + repo: provisioning.ResourceRepositoryInfo{ + Name: "test", }, client: &DynamicClient{}, kinds: &StaticKindsLookup{}, diff --git a/pkg/registry/apis/provisioning/resources/parser.go b/pkg/registry/apis/provisioning/resources/parser.go index ab010c68dc7..4ef33bce2a1 100644 --- a/pkg/registry/apis/provisioning/resources/parser.go +++ b/pkg/registry/apis/provisioning/resources/parser.go @@ -35,8 +35,15 @@ func (f *ParserFactory) GetParser(ctx context.Context, repo repository.Reader) ( return nil, err } + urls, _ := repo.(repository.RepositoryWithURLs) parser := &Parser{ - repo: config, + repo: provisioning.ResourceRepositoryInfo{ + Type: config.Spec.Type, + Title: config.Spec.Title, + Namespace: config.Namespace, + Name: config.Name, + }, + urls: urls, client: client, kinds: kinds, } @@ -46,7 +53,10 @@ func (f *ParserFactory) GetParser(ctx context.Context, repo repository.Reader) ( type Parser struct { // The target repository - repo *provisioning.Repository + repo provisioning.ResourceRepositoryInfo + + // for repositories that have URL support + urls repository.RepositoryWithURLs // client helper (for this namespace?) client *DynamicClient @@ -57,6 +67,12 @@ type ParsedResource struct { // Original file Info Info *repository.FileInfo + // The repository details + Repo provisioning.ResourceRepositoryInfo + + // Resource URLs + URLs *provisioning.ResourceURLs + // Check for classic file types (dashboard.json, etc) Classic provisioning.ClassicFileType @@ -94,6 +110,7 @@ func (r *Parser) Parse(ctx context.Context, info *repository.FileInfo, validate logger := logging.FromContext(ctx).With("path", info.Path, "validate", validate) parsed = &ParsedResource{ Info: info, + Repo: r.repo, } if ShouldIgnorePath(info.Path) && info.Path != "" { @@ -110,7 +127,15 @@ func (r *Parser) Parse(ctx context.Context, info *repository.FileInfo, validate } } - // Remove the internal UID,version and id if they exist + if r.urls != nil { + parsed.URLs, err = r.urls.ResourceURLs(ctx, info) + if err != nil { + logger.Debug("failed to load resource URLs", "error", err) + return parsed, err + } + } + + // Remove the internal dashboard UID,version and id if they exist if parsed.GVK.Group == dashboard.GROUP && parsed.GVK.Kind == "Dashboard" { unstructured.RemoveNestedField(parsed.Obj.Object, "spec", "uid") unstructured.RemoveNestedField(parsed.Obj.Object, "spec", "version") @@ -122,17 +147,16 @@ func (r *Parser) Parse(ctx context.Context, info *repository.FileInfo, validate return nil, err } obj := parsed.Obj - cfg := r.repo // Validate the namespace - if obj.GetNamespace() != "" && obj.GetNamespace() != cfg.GetNamespace() { + if obj.GetNamespace() != "" && obj.GetNamespace() != r.repo.Namespace { parsed.Errors = append(parsed.Errors, ErrNamespaceMismatch) } - obj.SetNamespace(cfg.GetNamespace()) + obj.SetNamespace(r.repo.Namespace) parsed.Meta.SetManagerProperties(utils.ManagerProperties{ Kind: utils.ManagerKindRepo, - Identity: cfg.Name, + Identity: r.repo.Name, }) parsed.Meta.SetSourceProperties(utils.SourceProperties{ Path: info.Path, // joinPathWithRef(info.Path, info.Ref), @@ -141,7 +165,7 @@ func (r *Parser) Parse(ctx context.Context, info *repository.FileInfo, validate // Calculate name+folder from the file path if info.Path != "" { - objName, folderName := NamesFromHashedRepoPath(cfg.Name, info.Path) + objName, folderName := NamesFromHashedRepoPath(r.repo.Name, info.Path) parsed.Meta.SetFolder(folderName) if obj.GetName() == "" { obj.SetName(objName) // use the name saved in config @@ -243,11 +267,13 @@ func (f *ParsedResource) AsResourceWrapper() *provisioning.ResourceWrapper { res.DryRun = v0alpha1.Unstructured{Object: f.DryRunResponse.Object} } wrap := &provisioning.ResourceWrapper{ - Path: info.Path, - Ref: info.Ref, - Hash: info.Hash, - Timestamp: info.Modified, - Resource: res, + Path: info.Path, + Ref: info.Ref, + Hash: info.Hash, + Repository: f.Repo, + URLs: f.URLs, + Timestamp: info.Modified, + Resource: res, } for _, err := range f.Errors { wrap.Errors = append(wrap.Errors, err.Error()) diff --git a/pkg/registry/apis/provisioning/sync.go b/pkg/registry/apis/provisioning/sync.go index 65b3dff4cac..e8919676a80 100644 --- a/pkg/registry/apis/provisioning/sync.go +++ b/pkg/registry/apis/provisioning/sync.go @@ -74,7 +74,7 @@ func (c *syncConnector) Connect( Spec: provisioning.JobSpec{ Action: provisioning.JobActionSync, Repository: cfg.Name, - Sync: &options, + Pull: &options, }, }) if err != nil { diff --git a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json index b5c484b0db4..e5529ce4b84 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -3171,22 +3171,14 @@ ], "properties": { "action": { - "description": "Possible enum values:\n - `\"export\"` Export from grafana into the remote repository\n - `\"migrate\"` Migration task -- this will migrate an full instance from SQL \u003e Git\n - `\"pr\"` Update a pull request -- send preview images, links etc\n - `\"sync\"` Sync the remote branch with the grafana instance", + "description": "Possible enum values:\n - `\"migrate\"` Migration task -- this will migrate an full instance from SQL \u003e Git\n - `\"pr\"` Process a pull request -- apply comments with preview images, links etc\n - `\"pull\"` Sync the remote branch with the grafana instance\n - `\"push\"` Export from grafana into the remote repository", "type": "string", "default": "", "enum": [ - "export", "migrate", "pr", - "sync" - ] - }, - "export": { - "description": "Required when the action is `export`", - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ExportJobOptions" - } + "pull", + "push" ] }, "migrate": { @@ -3205,18 +3197,26 @@ } ] }, - "repository": { - "description": "The the repository reference (for now also in labels)", - "type": "string", - "default": "" - }, - "sync": { - "description": "Required when the action is `sync`", + "pull": { + "description": "Required when the action is `pull`", "allOf": [ { "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.SyncJobOptions" } ] + }, + "push": { + "description": "Required when the action is `push`", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ExportJobOptions" + } + ] + }, + "repository": { + "description": "The the repository reference (for now also in labels)", + "type": "string", + "default": "" } } }, @@ -3776,6 +3776,41 @@ } } }, + "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ResourceRepositoryInfo": { + "type": "object", + "required": [ + "type", + "title", + "namespace", + "name" + ], + "properties": { + "name": { + "description": "The name (identifier)", + "type": "string", + "default": "" + }, + "namespace": { + "description": "The namespace this belongs to", + "type": "string", + "default": "" + }, + "title": { + "description": "The display name for this repository", + "type": "string", + "default": "" + }, + "type": { + "description": "The repository type\n\nPossible enum values:\n - `\"github\"`\n - `\"local\"`", + "type": "string", + "default": "", + "enum": [ + "github", + "local" + ] + } + } + }, "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ResourceStats": { "description": "Information we can get just from the file listing", "type": "object", @@ -3831,10 +3866,32 @@ } } }, + "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ResourceURLs": { + "type": "object", + "properties": { + "compareURL": { + "description": "Compare this version to the target branch", + "type": "string" + }, + "newPullRequestURL": { + "description": "A URL that will create a new pull requeset for this branch", + "type": "string" + }, + "repositoryURL": { + "description": "A URL pointing to the repository this lives in", + "type": "string" + }, + "sourceURL": { + "description": "A URL pointing to the this file in the repository", + "type": "string" + } + } + }, "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ResourceWrapper": { "description": "This is a container type for any resource type", "type": "object", "required": [ + "repository", "resource" ], "properties": { @@ -3864,9 +3921,18 @@ "type": "string" }, "ref": { - "description": "The commit hash (if exists)", + "description": "The request ref (or branch if exists)", "type": "string" }, + "repository": { + "description": "Basic repository info", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ResourceRepositoryInfo" + } + ] + }, "resource": { "description": "Different flavors of the same object", "default": {}, @@ -3883,6 +3949,14 @@ "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" } ] + }, + "urls": { + "description": "Typed links for this file (only supported by external systems, github etc)", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ResourceURLs" + } + ] } }, "x-kubernetes-group-version-kind": [ diff --git a/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx b/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx index 4a77a4dc796..6c966d4a7f3 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx @@ -1,6 +1,6 @@ import { Alert, Icon, Stack } from '@grafana/ui'; import { DashboardPageRouteSearchParams } from 'app/features/dashboard/containers/types'; -import { useGetRepositoryFilesWithPathQuery, useGetRepositoryQuery } from 'app/features/provisioning/api'; +import { useGetRepositoryFilesWithPathQuery } from 'app/features/provisioning/api'; import { usePullRequestParam } from 'app/features/provisioning/hooks'; import { DashboardRoutes } from 'app/types'; @@ -25,10 +25,6 @@ function DashboardPreviewBannerContent({ queryParams, slug, path }: DashboardPre const prParam = usePullRequestParam(); const file = useGetRepositoryFilesWithPathQuery({ name: slug, path, ref: queryParams.ref }); - // Required to create the new PR link - // TODO?? can the relevant links be in the file response??? - const repo = useGetRepositoryQuery({ name: slug }); - if (file.data?.errors) { return ( @@ -59,10 +55,8 @@ function DashboardPreviewBannerContent({ queryParams, slug, path }: DashboardPre } // Check if this is a github link - const githubSpec = repo.data?.spec?.type === 'github' ? repo.data.spec.github : undefined; - if (queryParams.ref?.length && githubSpec) { - const url = `${githubSpec.url}/compare/${githubSpec.branch}...${queryParams.ref}?quick_pull=1&labels=grafana`; - + const githubURL = file.data?.urls?.newPullRequestURL ?? file.data?.urls?.compareURL; + if (githubURL) { return ( } - onRemove={() => window.open(url, '_blank')} + onRemove={() => window.open(githubURL, '_blank')} > The value is not yet saved in the grafana database diff --git a/public/app/features/provisioning/api/endpoints.gen.ts b/public/app/features/provisioning/api/endpoints.gen.ts index 0bfdd7fa468..f0852965575 100644 --- a/public/app/features/provisioning/api/endpoints.gen.ts +++ b/public/app/features/provisioning/api/endpoints.gen.ts @@ -738,16 +738,6 @@ export type ObjectMeta = { Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ uid?: string; }; -export type ExportJobOptions = { - /** Target branch for export (only git) */ - branch?: string; - /** The source folder (or empty) to export */ - folder?: string; - /** Include the identifier in the exported metadata */ - identifier: boolean; - /** Prefix in target file system */ - prefix?: string; -}; export type MigrateJobOptions = { /** Preserve history (if possible) */ history?: boolean; @@ -769,23 +759,33 @@ export type SyncJobOptions = { /** Incremental synchronization for versioned repositories */ incremental: boolean; }; +export type ExportJobOptions = { + /** Target branch for export (only git) */ + branch?: string; + /** The source folder (or empty) to export */ + folder?: string; + /** Include the identifier in the exported metadata */ + identifier: boolean; + /** Prefix in target file system */ + prefix?: string; +}; export type JobSpec = { /** Possible enum values: - - `"export"` Export from grafana into the remote repository - `"migrate"` Migration task -- this will migrate an full instance from SQL > Git - - `"pr"` Update a pull request -- send preview images, links etc - - `"sync"` Sync the remote branch with the grafana instance */ - action: 'export' | 'migrate' | 'pr' | 'sync'; - /** Required when the action is `export` */ - export?: ExportJobOptions; + - `"pr"` Process a pull request -- apply comments with preview images, links etc + - `"pull"` Sync the remote branch with the grafana instance + - `"push"` Export from grafana into the remote repository */ + action: 'migrate' | 'pr' | 'pull' | 'push'; /** Required when the action is `migrate` */ migrate?: MigrateJobOptions; /** Pull request options */ pr?: PullRequestJobOptions; + /** Required when the action is `pull` */ + pull?: SyncJobOptions; + /** Required when the action is `push` */ + push?: ExportJobOptions; /** The the repository reference (for now also in labels) */ repository: string; - /** Required when the action is `sync` */ - sync?: SyncJobOptions; }; export type JobResourceSummary = { create?: number; @@ -1034,6 +1034,20 @@ export type DeleteOptions = { /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ propagationPolicy?: string; }; +export type ResourceRepositoryInfo = { + /** The name (identifier) */ + name: string; + /** The namespace this belongs to */ + namespace: string; + /** The display name for this repository */ + title: string; + /** The repository type + + Possible enum values: + - `"github"` + - `"local"` */ + type: 'github' | 'local'; +}; export type Unstructured = { [key: string]: any; }; @@ -1068,6 +1082,16 @@ export type ResourceObjects = { /** The identified type for this object */ type: ResourceType; }; +export type ResourceUrLs = { + /** Compare this version to the target branch */ + compareURL?: string; + /** A URL that will create a new pull requeset for this branch */ + newPullRequestURL?: string; + /** A URL pointing to the repository this lives in */ + repositoryURL?: string; + /** A URL pointing to the this file in the repository */ + sourceURL?: string; +}; export type ResourceWrapper = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion?: string; @@ -1079,12 +1103,16 @@ export type ResourceWrapper = { kind?: string; /** Path to the remote file */ path?: string; - /** The commit hash (if exists) */ + /** The request ref (or branch if exists) */ ref?: string; + /** Basic repository info */ + repository: ResourceRepositoryInfo; /** Different flavors of the same object */ resource: ResourceObjects; /** The modified time in the remote file system */ timestamp?: Time; + /** Typed links for this file (only supported by external systems, github etc) */ + urls?: ResourceUrLs; }; export type ResourceListItem = { folder?: string;