From 151d9ec1cc72baea6574849b02b6de185e1fbe84 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Mon, 17 Nov 2025 12:48:53 +0100 Subject: [PATCH] Advisor: Expose endpoint to register checktypes (#113750) --- apps/advisor/kinds/manifest.cue | 11 +++ .../createregister_response_body_types_gen.go | 13 ++++ ...reateregister_response_object_types_gen.go | 37 ++++++++++ apps/advisor/pkg/apis/advisor_manifest.go | 61 +++++++++++++++- apps/advisor/pkg/app/app.go | 52 +++++++++++-- .../checktyperegisterer.go | 73 +++++++++++-------- .../rtkq/advisor/v0alpha1/endpoints.gen.ts | 13 ++++ .../advisor.grafana.app-v0alpha1.json | 70 ++++++++++++++++++ 8 files changed, 286 insertions(+), 44 deletions(-) create mode 100644 apps/advisor/pkg/apis/advisor/v0alpha1/createregister_response_body_types_gen.go create mode 100644 apps/advisor/pkg/apis/advisor/v0alpha1/createregister_response_object_types_gen.go diff --git a/apps/advisor/kinds/manifest.cue b/apps/advisor/kinds/manifest.cue index ca791996f13..d478c973c32 100644 --- a/apps/advisor/kinds/manifest.cue +++ b/apps/advisor/kinds/manifest.cue @@ -13,6 +13,17 @@ manifest: { checkv0alpha1, checktypev0alpha1, ] + routes: { + namespaced: { + "/register": { + "POST": { + response: { + message: string + } + } + } + } + } } } } diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/createregister_response_body_types_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/createregister_response_body_types_gen.go new file mode 100644 index 00000000000..2dfdfd2ccfb --- /dev/null +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/createregister_response_body_types_gen.go @@ -0,0 +1,13 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type CreateRegisterBody struct { + Message string `json:"message"` +} + +// NewCreateRegisterBody creates a new CreateRegisterBody object. +func NewCreateRegisterBody() *CreateRegisterBody { + return &CreateRegisterBody{} +} diff --git a/apps/advisor/pkg/apis/advisor/v0alpha1/createregister_response_object_types_gen.go b/apps/advisor/pkg/apis/advisor/v0alpha1/createregister_response_object_types_gen.go new file mode 100644 index 00000000000..c5bff31a374 --- /dev/null +++ b/apps/advisor/pkg/apis/advisor/v0alpha1/createregister_response_object_types_gen.go @@ -0,0 +1,37 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +k8s:openapi-gen=true +type CreateRegister struct { + metav1.TypeMeta `json:",inline"` + CreateRegisterBody `json:",inline"` +} + +func NewCreateRegister() *CreateRegister { + return &CreateRegister{} +} + +func (t *CreateRegisterBody) DeepCopyInto(dst *CreateRegisterBody) { + _ = resource.CopyObjectInto(dst, t) +} + +func (o *CreateRegister) DeepCopyObject() runtime.Object { + dst := NewCreateRegister() + o.DeepCopyInto(dst) + return dst +} + +func (o *CreateRegister) DeepCopyInto(dst *CreateRegister) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.CreateRegisterBody.DeepCopyInto(&dst.CreateRegisterBody) +} + +var _ runtime.Object = NewCreateRegister() diff --git a/apps/advisor/pkg/apis/advisor_manifest.go b/apps/advisor/pkg/apis/advisor_manifest.go index 5c7b5896ee8..8b6e5afe3bf 100644 --- a/apps/advisor/pkg/apis/advisor_manifest.go +++ b/apps/advisor/pkg/apis/advisor_manifest.go @@ -62,9 +62,60 @@ var appManifestData = app.ManifestData{ }, }, Routes: app.ManifestVersionRoutes{ - Namespaced: map[string]spec3.PathProps{}, - Cluster: map[string]spec3.PathProps{}, - Schemas: map[string]spec.Schema{}, + Namespaced: map[string]spec3.PathProps{ + "/register": { + Post: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "createRegister", + + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + Default: &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Default OK response", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + 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", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + 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", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + Required: []string{ + "message", + "apiVersion", + "kind", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, + Cluster: map[string]spec3.PathProps{}, + Schemas: map[string]spec.Schema{}, }, }, }, @@ -90,7 +141,9 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist return goType, exists } -var customRouteToGoResponseType = map[string]any{} +var customRouteToGoResponseType = map[string]any{ + "v0alpha1||/register|POST": v0alpha1.CreateRegister{}, +} // ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. // kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. diff --git a/apps/advisor/pkg/app/app.go b/apps/advisor/pkg/app/app.go index d4019631ef6..9c1dd3c0f98 100644 --- a/apps/advisor/pkg/app/app.go +++ b/apps/advisor/pkg/app/app.go @@ -2,7 +2,9 @@ package app import ( "context" + "encoding/json" "fmt" + "net/http" "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/k8s" @@ -16,6 +18,7 @@ import ( "github.com/grafana/grafana/apps/advisor/pkg/app/checkscheduler" "github.com/grafana/grafana/apps/advisor/pkg/app/checktyperegisterer" "github.com/grafana/grafana/pkg/apimachinery/identity" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -49,6 +52,16 @@ func New(cfg app.Config) (app.App, error) { checkMap[c.ID()] = c } + ctr, err := checktyperegisterer.New(cfg, log) + if err != nil { + return nil, err + } + + csch, err := checkscheduler.New(cfg, log) + if err != nil { + return nil, err + } + simpleConfig := simple.AppConfig{ Name: "advisor", KubeConfig: cfg.KubeConfig, @@ -110,6 +123,37 @@ func New(cfg app.Config) (app.App, error) { Kind: advisorv0alpha1.CheckTypeKind(), }, }, + VersionedCustomRoutes: map[string]simple.AppVersionRouteHandlers{ + "v0alpha1": { + { + Namespaced: true, + Path: "register", + Method: "POST", + }: func(ctx context.Context, w app.CustomRouteResponseWriter, req *app.CustomRouteRequest) error { + logger := log.WithContext(ctx) + namespace := req.ResourceIdentifier.Namespace + + // Register check types for the namespace + err := ctr.RegisterCheckTypesInNamespace(ctx, logger, namespace) + if err != nil { + logger.Error("Failed to register check types", "namespace", namespace, "error", err) + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return err + } + + // Return typed response matching the manifest + return json.NewEncoder(w).Encode(advisorv0alpha1.CreateRegister{ + TypeMeta: metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", advisorv0alpha1.APIGroup, advisorv0alpha1.APIVersion), + }, + CreateRegisterBody: advisorv0alpha1.CreateRegisterBody{ + Message: "Check types registered successfully", + }, + }) + }, + }, + }, } a, err := simple.NewApp(simpleConfig) @@ -123,17 +167,9 @@ func New(cfg app.Config) (app.App, error) { } // Save check types as resources - ctr, err := checktyperegisterer.New(cfg, log) - if err != nil { - return nil, err - } a.AddRunnable(ctr) // Start scheduler - csch, err := checkscheduler.New(cfg, log) - if err != nil { - return nil, err - } a.AddRunnable(csch) return a, nil diff --git a/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go b/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go index 8f1675c8ccb..e50250c47d0 100644 --- a/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go +++ b/apps/advisor/pkg/app/checktyperegisterer/checktyperegisterer.go @@ -34,8 +34,10 @@ type Runner struct { retryDelay time.Duration } +var _ app.Runnable = (*Runner)(nil) + // NewRunner creates a new Runner. -func New(cfg app.Config, log logging.Logger) (app.Runnable, error) { +func New(cfg app.Config, log logging.Logger) (*Runner, error) { // Read config specificConfig, ok := cfg.SpecificConfig.(checkregistry.AdvisorAppConfig) if !ok { @@ -79,37 +81,9 @@ func (r *Runner) Run(ctx context.Context) error { // Register check types in each namespace for _, namespace := range namespaces { - for _, t := range r.checkRegistry.Checks() { - steps := t.Steps() - stepTypes := make([]advisorv0alpha1.CheckTypeStep, len(steps)) - for i, s := range steps { - stepTypes[i] = advisorv0alpha1.CheckTypeStep{ - Title: s.Title(), - Description: s.Description(), - StepID: s.ID(), - Resolution: s.Resolution(), - } - } - obj := &advisorv0alpha1.CheckType{ - ObjectMeta: metav1.ObjectMeta{ - Name: t.ID(), - Namespace: namespace, - Annotations: map[string]string{ - checks.NameAnnotation: t.Name(), - // Flag to indicate feature availability - checks.RetryAnnotation: "1", - checks.IgnoreStepsAnnotation: "1", - }, - }, - Spec: advisorv0alpha1.CheckTypeSpec{ - Name: t.ID(), - Steps: stepTypes, - }, - } - err := r.registerCheckType(ctx, logger, t.ID(), obj) - if err != nil { - return err - } + err := r.RegisterCheckTypesInNamespace(ctx, logger, namespace) + if err != nil { + return fmt.Errorf("failed to register check types in namespace %s: %w", namespace, err) } } return nil @@ -243,3 +217,38 @@ func isAPIServerShuttingDown(err error, logger logging.Logger) bool { } return false } + +func (r *Runner) RegisterCheckTypesInNamespace(ctx context.Context, logger logging.Logger, namespace string) error { + for _, t := range r.checkRegistry.Checks() { + steps := t.Steps() + stepTypes := make([]advisorv0alpha1.CheckTypeStep, len(steps)) + for i, s := range steps { + stepTypes[i] = advisorv0alpha1.CheckTypeStep{ + Title: s.Title(), + Description: s.Description(), + StepID: s.ID(), + Resolution: s.Resolution(), + } + } + obj := &advisorv0alpha1.CheckType{ + ObjectMeta: metav1.ObjectMeta{ + Name: t.ID(), + Namespace: namespace, + Annotations: map[string]string{ + checks.NameAnnotation: t.Name(), + // Flag to indicate feature availability + checks.RetryAnnotation: "1", + checks.IgnoreStepsAnnotation: "1", + }, + }, + Spec: advisorv0alpha1.CheckTypeSpec{ + Name: t.ID(), + Steps: stepTypes, + }, + } + if err := r.registerCheckType(ctx, logger, t.ID(), obj); err != nil { + return fmt.Errorf("failed to register check type %s: %w", t.ID(), err) + } + } + return nil +} diff --git a/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/endpoints.gen.ts index 84bbfa33c91..56f00f682f4 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/advisor/v0alpha1/endpoints.gen.ts @@ -304,6 +304,9 @@ const injectedRtkApi = api }), invalidatesTags: ['CheckType'], }), + createRegister: build.mutation({ + query: () => ({ url: `/register`, method: 'POST' }), + }), }), overrideExisting: false, }); @@ -714,6 +717,8 @@ export type UpdateCheckTypeStatusApiArg = { force?: boolean; patch: Patch; }; +export type CreateRegisterApiResponse = /** status 200 OK */ CreateRegister; +export type CreateRegisterApiArg = void; export type ApiResource = { /** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */ categories?: string[]; @@ -1016,6 +1021,13 @@ export type CheckTypeList = { kind?: string; metadata: ListMeta; }; +export type CreateRegister = { + /** 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; + /** 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; + message: string; +}; export const { useGetApiResourcesQuery, useLazyGetApiResourcesQuery, @@ -1045,4 +1057,5 @@ export const { useLazyGetCheckTypeStatusQuery, useReplaceCheckTypeStatusMutation, useUpdateCheckTypeStatusMutation, + useCreateRegisterMutation, } = injectedRtkApi; diff --git a/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json index f9816e06ce1..78fb056058a 100644 --- a/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/advisor.grafana.app-v0alpha1.json @@ -2274,6 +2274,55 @@ } } ] + }, + "/apis/advisor.grafana.app/v0alpha1/namespaces/{namespace}/register": { + "post": { + "operationId": "createRegister", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CreateRegister" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CreateRegister" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CreateRegister" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CreateRegister" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CreateRegister" + } + } + } + } + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] } }, "components": { @@ -2701,6 +2750,27 @@ }, "additionalProperties": false }, + "com.github.grafana.grafana.apps.advisor.pkg.apis.advisor.v0alpha1.CreateRegister": { + "type": "object", + "required": [ + "message", + "apiVersion", + "kind" + ], + "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" + }, + "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" + }, + "message": { + "type": "string" + } + } + }, "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { "description": "APIResource specifies the name of a resource and whether it is namespaced.", "type": "object",