POC/Provisioning: Expose repositories without the full configuration details (#99791)

This commit is contained in:
Ryan McKinley
2025-01-31 10:43:14 +03:00
committed by GitHub
parent f5ef63bcec
commit 966bb8a1e4
7 changed files with 422 additions and 73 deletions
@@ -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"`
}
@@ -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
@@ -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{
@@ -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 {
+161
View File
@@ -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)
}
@@ -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",
@@ -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<GetFrontendSettingsResponse, GetFrontendSettingsArg>({
query: (queryArg) => ({ url: `/settings` }),
providesTags: ['Provisioning'],
}),
getResourceStats: build.query<GetResourceStatsResponse, GetResourceStatsArg>({
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;