diff --git a/pkg/apis/provisioning/v0alpha1/settings.go b/pkg/apis/provisioning/v0alpha1/settings.go new file mode 100644 index 00000000000..4d412bdfa64 --- /dev/null +++ b/pkg/apis/provisioning/v0alpha1/settings.go @@ -0,0 +1,31 @@ +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Summary shows a view of the configuration that is sanitized and is OK for logged in users to see +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type Settings struct { + metav1.TypeMeta `json:",inline"` + + // When a repository is configured to save everything in instance + Instance string `json:"instance,omitempty"` + + // The basic repository settings + Repository map[string]RepositoryView `json:"repository"` +} + +type RepositoryView struct { + // Repository display + Title string `json:"title"` + + // Edit options within the repository + ReadOnly bool `json:"readOnly"` + + // The repository type + Type RepositoryType `json:"type"` + + // When syncing, where values are saved + Target SyncTargetType `json:"target"` +} diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go index b3734c25d79..63611a009ed 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go @@ -516,6 +516,22 @@ func (in *RepositoryStatus) DeepCopy() *RepositoryStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RepositoryView) DeepCopyInto(out *RepositoryView) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RepositoryView. +func (in *RepositoryView) DeepCopy() *RepositoryView { + if in == nil { + return nil + } + out := new(RepositoryView) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResourceCount) DeepCopyInto(out *ResourceCount) { *out = *in @@ -702,6 +718,38 @@ func (in *S3RepositoryConfig) DeepCopy() *S3RepositoryConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Settings) DeepCopyInto(out *Settings) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Repository != nil { + in, out := &in.Repository, &out.Repository + *out = make(map[string]RepositoryView, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Settings. +func (in *Settings) DeepCopy() *Settings { + if in == nil { + return nil + } + out := new(Settings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Settings) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SyncJobOptions) DeepCopyInto(out *SyncJobOptions) { *out = *in diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index a163fe1e78c..a978dc83570 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -35,6 +35,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryList": schema_pkg_apis_provisioning_v0alpha1_RepositoryList(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositorySpec": schema_pkg_apis_provisioning_v0alpha1_RepositorySpec(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryStatus": schema_pkg_apis_provisioning_v0alpha1_RepositoryStatus(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryView": schema_pkg_apis_provisioning_v0alpha1_RepositoryView(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceCount": schema_pkg_apis_provisioning_v0alpha1_ResourceCount(ref), "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), @@ -43,6 +44,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceType": schema_pkg_apis_provisioning_v0alpha1_ResourceType(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.S3RepositoryConfig": schema_pkg_apis_provisioning_v0alpha1_S3RepositoryConfig(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Settings": schema_pkg_apis_provisioning_v0alpha1_Settings(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), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.SyncStatus": schema_pkg_apis_provisioning_v0alpha1_SyncStatus(ref), @@ -1045,6 +1047,53 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositoryStatus(ref common.Reference } } +func schema_pkg_apis_provisioning_v0alpha1_RepositoryView(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Description: "Repository display", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "readOnly": { + SchemaProps: spec.SchemaProps{ + Description: "Edit options within the repository", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "The repository type\n\nPossible enum values:\n - `\"github\"`\n - `\"local\"`\n - `\"s3\"`", + Default: "", + Type: []string{"string"}, + Format: "", + Enum: []interface{}{"github", "local", "s3"}, + }, + }, + "target": { + SchemaProps: spec.SchemaProps{ + Description: "When syncing, where values are saved\n\nPossible enum values:\n - `\"folder\"` Resources will be saved into a folder managed by this repository The folder k8s name will be the same as the repository k8s name It will contain a copy of everything from the remote\n - `\"instance\"` Resources are saved in the global context Only one repository may specify the `instance` target When this exists, the UI will promote writing to the instance repo rather than the grafana database (where possible)", + Default: "", + Type: []string{"string"}, + Format: "", + Enum: []interface{}{"folder", "instance"}, + }, + }, + }, + Required: []string{"title", "readOnly", "type", "target"}, + }, + }, + } +} + func schema_pkg_apis_provisioning_v0alpha1_ResourceCount(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1457,6 +1506,58 @@ func schema_pkg_apis_provisioning_v0alpha1_S3RepositoryConfig(ref common.Referen } } +func schema_pkg_apis_provisioning_v0alpha1_Settings(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Summary shows a view of the configuration that is sanitized and is OK for logged in users to see", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "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", + Type: []string{"string"}, + Format: "", + }, + }, + "instance": { + SchemaProps: spec.SchemaProps{ + Description: "When a repository is configured to save everything in instance", + Type: []string{"string"}, + Format: "", + }, + }, + "repository": { + SchemaProps: spec.SchemaProps{ + Description: "The basic repository settings", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryView"), + }, + }, + }, + }, + }, + }, + Required: []string{"repository"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.RepositoryView"}, + } +} + func schema_pkg_apis_provisioning_v0alpha1_SyncJobOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index 51d71aa8698..e93b543036b 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -2,7 +2,6 @@ package provisioning import ( "context" - "encoding/json" "fmt" "net/http" "time" @@ -23,7 +22,6 @@ import ( "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" - authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" @@ -44,7 +42,6 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/blob" "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/grafana/grafana/pkg/util/errhttp" ) const repoControllerWorkers = 1 @@ -414,73 +411,6 @@ func (b *APIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { return provisioning.GetOpenAPIDefinitions } -func (b *APIBuilder) GetAPIRoutes() *builder.APIRoutes { - defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} }) - - statsResult := defs["github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceStats"].Schema - - return &builder.APIRoutes{ - Namespace: []builder.APIRouteHandler{ - { - Path: "stats", - Spec: &spec3.PathProps{ - Get: &spec3.Operation{ - OperationProps: spec3.OperationProps{ - OperationId: "getResourceStats", // used for RTK client - Tags: []string{"Repository"}, - Description: "Get resource stats for this namespace", - Parameters: []*spec3.Parameter{ - { - ParameterProps: spec3.ParameterProps{ - Name: "namespace", - In: "path", - Required: true, - Example: "default", - Description: "workspace", - Schema: spec.StringProperty(), - }, - }, - }, - Responses: &spec3.Responses{ - ResponsesProps: spec3.ResponsesProps{ - StatusCodeResponses: map[int]*spec3.Response{ - 200: { - ResponseProps: spec3.ResponseProps{ - Content: map[string]*spec3.MediaType{ - "application/json": { - MediaTypeProps: spec3.MediaTypeProps{ - Schema: &statsResult, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - Handler: func(w http.ResponseWriter, r *http.Request) { - u, ok := authlib.AuthInfoFrom(r.Context()) - if !ok { - w.WriteHeader(400) - _, _ = w.Write([]byte("expected user")) - return - } - stats, err := b.resourceLister.Stats(r.Context(), u.GetNamespace(), "") - if err != nil { - errhttp.Write(r.Context(), err, w) - return - } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(stats) - }, - }, - }, - } -} - func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartHookFunc, error) { postStartHooks := map[string]genericapiserver.PostStartHookFunc{ "grafana-provisioning": func(postStartHookCtx genericapiserver.PostStartHookContext) error { diff --git a/pkg/registry/apis/provisioning/routes.go b/pkg/registry/apis/provisioning/routes.go new file mode 100644 index 00000000000..9a524f9b1d3 --- /dev/null +++ b/pkg/registry/apis/provisioning/routes.go @@ -0,0 +1,161 @@ +package provisioning + +import ( + "encoding/json" + "fmt" + "net/http" + + "k8s.io/apimachinery/pkg/labels" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" + + authlib "github.com/grafana/authlib/types" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/util/errhttp" +) + +// GetAPIRoutes implements the direct HTTP handlers that bypass k8s +func (b *APIBuilder) GetAPIRoutes() *builder.APIRoutes { + defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} }) + + schemaStats := defs["github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ResourceStats"].Schema + schemaSettings := defs["github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Settings"].Schema + + return &builder.APIRoutes{ + Namespace: []builder.APIRouteHandler{ + { + Path: "stats", + Spec: &spec3.PathProps{ + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + OperationId: "getResourceStats", // used for RTK client + Tags: []string{"Provisioning"}, + Description: "Get resource stats for this namespace", + Parameters: []*spec3.Parameter{ + { + ParameterProps: spec3.ParameterProps{ + Name: "namespace", + In: "path", + Required: true, + Example: "default", + Description: "workspace", + Schema: spec.StringProperty(), + }, + }, + }, + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + StatusCodeResponses: map[int]*spec3.Response{ + 200: { + ResponseProps: spec3.ResponseProps{ + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &schemaStats, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Handler: b.handleStats, + }, + { + Path: "settings", + Spec: &spec3.PathProps{ + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + OperationId: "getFrontendSettings", // used for RTK client + Tags: []string{"Provisioning"}, + Description: "Get the frontend settings for this namespace", + Parameters: []*spec3.Parameter{ + { + ParameterProps: spec3.ParameterProps{ + Name: "namespace", + In: "path", + Required: true, + Example: "default", + Description: "workspace", + Schema: spec.StringProperty(), + }, + }, + }, + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + StatusCodeResponses: map[int]*spec3.Response{ + 200: { + ResponseProps: spec3.ResponseProps{ + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &schemaSettings, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Handler: b.handleSettings, + }, + }, + } +} + +func (b *APIBuilder) handleStats(w http.ResponseWriter, r *http.Request) { + u, ok := authlib.AuthInfoFrom(r.Context()) + if !ok { + w.WriteHeader(400) + _, _ = w.Write([]byte("expected user")) + return + } + stats, err := b.resourceLister.Stats(r.Context(), u.GetNamespace(), "") + if err != nil { + errhttp.Write(r.Context(), err, w) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(stats) +} + +func (b *APIBuilder) handleSettings(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + u, ok := authlib.AuthInfoFrom(ctx) + if !ok { + errhttp.Write(ctx, fmt.Errorf("expected user"), w) + return + } + + all, err := b.repositoryLister.Repositories(u.GetNamespace()).List(labels.Everything()) + if err != nil { + errhttp.Write(r.Context(), err, w) + return + } + + settings := provisioning.Settings{ + Repository: make(map[string]provisioning.RepositoryView, len(all)+2), + } + for _, val := range all { + settings.Repository[val.Name] = provisioning.RepositoryView{ + Title: val.Spec.Title, + Type: val.Spec.Type, + ReadOnly: val.Spec.ReadOnly, + Target: val.Spec.Sync.Target, + } + if val.Spec.Sync.Target == provisioning.SyncTargetTypeInstance { + settings.Instance = val.Name + } + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(settings) +} 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 10a8b667905..69510eedfcf 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -2445,10 +2445,67 @@ } ] }, + "/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/settings": { + "get": { + "tags": [ + "Provisioning" + ], + "description": "Get the frontend settings for this namespace", + "operationId": "getFrontendSettings", + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "workspace", + "required": true, + "schema": { + "type": "string" + }, + "example": "default" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "description": "Summary shows a view of the configuration that is sanitized and is OK for logged in users to see", + "type": "object", + "required": [ + "repository" + ], + "properties": { + "apiVersion": { + "description": "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", + "type": "string" + }, + "instance": { + "description": "When a repository is configured to save everything in instance", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "repository": { + "description": "The basic repository settings", + "type": "object", + "additionalProperties": { + "default": {} + } + } + } + } + } + } + } + } + } + }, "/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/stats": { "get": { "tags": [ - "Repository" + "Provisioning" ], "description": "Get resource stats for this namespace", "operationId": "getResourceStats", diff --git a/public/app/features/provisioning/api/endpoints.gen.ts b/public/app/features/provisioning/api/endpoints.gen.ts index fc6f3414c68..3fd884a7068 100644 --- a/public/app/features/provisioning/api/endpoints.gen.ts +++ b/public/app/features/provisioning/api/endpoints.gen.ts @@ -1,5 +1,5 @@ import { baseAPI as api } from './baseAPI'; -export const addTagTypes = ['Job', 'Repository'] as const; +export const addTagTypes = ['Job', 'Repository', 'Provisioning'] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, @@ -233,9 +233,13 @@ const injectedRtkApi = api query: (queryArg) => ({ url: `/repositories/${queryArg.name}/webhook`, method: 'POST' }), invalidatesTags: ['Repository'], }), + getFrontendSettings: build.query({ + query: (queryArg) => ({ url: `/settings` }), + providesTags: ['Provisioning'], + }), getResourceStats: build.query({ query: (queryArg) => ({ url: `/stats` }), - providesTags: ['Repository'], + providesTags: ['Provisioning'], }), }), overrideExisting: false, @@ -544,6 +548,22 @@ export type CreateRepositoryWebhookArg = { /** name of the WebhookResponse */ name: string; }; +export type GetFrontendSettingsResponse = /** status 200 undefined */ { + /** 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; + /** When a repository is configured to save everything in instance */ + instance?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** The basic repository settings */ + repository: { + [key: string]: any; + }; +}; +export type GetFrontendSettingsArg = { + /** workspace */ + namespace: string; +}; export type GetResourceStatsResponse = /** status 200 undefined */ { /** 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; @@ -1058,5 +1078,6 @@ export const { useCreateRepositoryTestMutation, useGetRepositoryWebhookQuery, useCreateRepositoryWebhookMutation, + useGetFrontendSettingsQuery, useGetResourceStatsQuery, } = injectedRtkApi;