From 74a2232de1f428f8c42249f408cdcc700ca76c29 Mon Sep 17 00:00:00 2001 From: Sarah Zinger Date: Thu, 2 Oct 2025 15:40:30 -0400 Subject: [PATCH 01/14] DS-Apiservers: fix error handling in ds apiservers (#111956) --- pkg/registry/apis/datasource/sub_query.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/registry/apis/datasource/sub_query.go b/pkg/registry/apis/datasource/sub_query.go index 51f8b90d430..258447418bc 100644 --- a/pkg/registry/apis/datasource/sub_query.go +++ b/pkg/registry/apis/datasource/sub_query.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/errutil" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" query_headers "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/services/datasources" @@ -96,6 +97,22 @@ func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Ob PluginContext: pluginCtx, Headers: query_headers.ExtractKnownHeaders(req.Header), }) + + // all errors get converted into k8 errors when sent in responder.Error and lose important context like downstream info + var e errutil.Error + if errors.As(err, &e) && e.Source == errutil.SourceDownstream { + responder.Object(int(backend.StatusBadRequest), + &query.QueryDataResponse{QueryDataResponse: backend.QueryDataResponse{Responses: map[string]backend.DataResponse{ + "A": { + Error: errors.New(e.LogMessage), + ErrorSource: backend.ErrorSourceDownstream, + Status: backend.StatusBadRequest, + }, + }}}, + ) + return + } + if err != nil { responder.Error(err) return From 22173da78d0d3f07a5ae69d79b1f9195a150c8d2 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 2 Oct 2025 15:46:59 -0400 Subject: [PATCH 02/14] Alerting: Use empty feature manager for creating test state (#111964) --- pkg/services/ngalert/schedule/alert_rule_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index 1ae84649fbd..48be6bb4a40 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -1317,7 +1317,7 @@ func stateForRule(rule *models.AlertRule, ts time.Time, evalState eval.State) *s for k, v := range rule.Labels { s.Labels[k] = v } - for k, v := range state.GetRuleExtraLabels(&logtest.Fake{}, rule, "", true, nil) { + for k, v := range state.GetRuleExtraLabels(&logtest.Fake{}, rule, "", true, featuremgmt.WithFeatures()) { if _, ok := s.Labels[k]; !ok { s.Labels[k] = v } From 13d9829836c9a8269b5df473e15a25d660c9df38 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 2 Oct 2025 23:15:39 +0300 Subject: [PATCH 03/14] Chore: Remove unused status element (#111963) --- apps/correlations/Makefile | 1 + .../v0alpha1/correlation_client_gen.go | 19 -- .../v0alpha1/correlation_object_gen.go | 28 +- .../v0alpha1/correlation_status_gen.go | 44 --- .../pkg/apis/correlation_manifest.go | 7 +- .../v0alpha1/correlation_object_gen.ts | 2 - .../correlation/v0alpha1/types.status.gen.ts | 30 -- apps/iam/Makefile | 1 + .../apis/iam/v0alpha1/corerole_client_gen.go | 19 -- .../apis/iam/v0alpha1/corerole_object_gen.go | 28 +- .../iam/v0alpha1/globalrole_client_gen.go | 19 -- .../iam/v0alpha1/globalrole_object_gen.go | 28 +- .../v0alpha1/globalrolebinding_client_gen.go | 19 -- .../v0alpha1/globalrolebinding_object_gen.go | 28 +- .../v0alpha1/resourcepermission_client_gen.go | 19 -- .../v0alpha1/resourcepermission_object_gen.go | 28 +- .../pkg/apis/iam/v0alpha1/role_client_gen.go | 19 -- .../pkg/apis/iam/v0alpha1/role_object_gen.go | 28 +- .../iam/v0alpha1/rolebinding_client_gen.go | 19 -- .../iam/v0alpha1/rolebinding_object_gen.go | 28 +- .../iam/v0alpha1/serviceaccount_client_gen.go | 19 -- .../iam/v0alpha1/serviceaccount_object_gen.go | 28 +- .../pkg/apis/iam/v0alpha1/team_client_gen.go | 19 -- .../pkg/apis/iam/v0alpha1/team_object_gen.go | 28 +- .../iam/v0alpha1/teambinding_client_gen.go | 19 -- .../iam/v0alpha1/teambinding_object_gen.go | 28 +- .../pkg/apis/iam/v0alpha1/user_client_gen.go | 19 -- .../pkg/apis/iam/v0alpha1/user_object_gen.go | 28 +- .../pkg/apis/iam/v0alpha1/zz_openapi_gen.go | 100 ++---- apps/preferences/Makefile | 1 + .../v1alpha1/preferences_client_gen.go | 19 -- .../v1alpha1/preferences_object_gen.go | 28 +- .../v1alpha1/preferences_status_gen.go | 44 --- .../preferences/v1alpha1/stars_client_gen.go | 19 -- .../preferences/v1alpha1/stars_object_gen.go | 28 +- .../preferences/v1alpha1/stars_status_gen.go | 44 --- .../v1alpha1/zz_generated.openapi.go | 214 +----------- .../pkg/apis/preferences_manifest.go | 4 +- apps/provisioning/Makefile | 9 +- pkg/tests/apis/iam/iam_test.go | 3 +- .../iam.grafana.app-v0alpha1.json | 320 +----------------- .../preferences.grafana.app-v1alpha1.json | 130 +------ .../preferences/v1alpha1/endpoints.gen.ts | 46 --- 43 files changed, 69 insertions(+), 1542 deletions(-) delete mode 100644 apps/correlations/pkg/apis/correlation/v0alpha1/correlation_status_gen.go delete mode 100644 apps/correlations/plugin/src/generated/correlation/v0alpha1/types.status.gen.ts delete mode 100644 apps/preferences/pkg/apis/preferences/v1alpha1/preferences_status_gen.go delete mode 100644 apps/preferences/pkg/apis/preferences/v1alpha1/stars_status_gen.go diff --git a/apps/correlations/Makefile b/apps/correlations/Makefile index 230bfd4149a..bc8d6d30cb5 100644 --- a/apps/correlations/Makefile +++ b/apps/correlations/Makefile @@ -6,4 +6,5 @@ generate: install-app-sdk update-app-sdk --source=./kinds/ \ --gogenpath=./pkg/apis \ --grouping=group \ + --genoperatorstate=false \ --defencoding=none \ No newline at end of file diff --git a/apps/correlations/pkg/apis/correlation/v0alpha1/correlation_client_gen.go b/apps/correlations/pkg/apis/correlation/v0alpha1/correlation_client_gen.go index 482d497e154..fe8f518b852 100644 --- a/apps/correlations/pkg/apis/correlation/v0alpha1/correlation_client_gen.go +++ b/apps/correlations/pkg/apis/correlation/v0alpha1/correlation_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type CorrelationClient struct { @@ -76,24 +75,6 @@ func (c *CorrelationClient) Patch(ctx context.Context, identifier resource.Ident return c.client.Patch(ctx, identifier, req, opts) } -func (c *CorrelationClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus CorrelationStatus, opts resource.UpdateOptions) (*Correlation, error) { - return c.client.Update(ctx, &Correlation{ - TypeMeta: metav1.TypeMeta{ - Kind: CorrelationKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *CorrelationClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/correlations/pkg/apis/correlation/v0alpha1/correlation_object_gen.go b/apps/correlations/pkg/apis/correlation/v0alpha1/correlation_object_gen.go index d10cb114aa8..3191e415977 100644 --- a/apps/correlations/pkg/apis/correlation/v0alpha1/correlation_object_gen.go +++ b/apps/correlations/pkg/apis/correlation/v0alpha1/correlation_object_gen.go @@ -21,8 +21,6 @@ type Correlation struct { // Spec is the spec of the Correlation Spec CorrelationSpec `json:"spec" yaml:"spec"` - - Status CorrelationStatus `json:"status" yaml:"status"` } func (o *Correlation) GetSpec() any { @@ -39,15 +37,11 @@ func (o *Correlation) SetSpec(spec any) error { } func (o *Correlation) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *Correlation) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *Correlation) GetSubresource(name string) (any, bool) { func (o *Correlation) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(CorrelationStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type CorrelationStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *Correlation) DeepCopyInto(dst *Correlation) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *CorrelationSpec) DeepCopy() *CorrelationSpec { func (s *CorrelationSpec) DeepCopyInto(dst *CorrelationSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of CorrelationStatus -func (s *CorrelationStatus) DeepCopy() *CorrelationStatus { - cpy := &CorrelationStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies CorrelationStatus into another CorrelationStatus object -func (s *CorrelationStatus) DeepCopyInto(dst *CorrelationStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/correlations/pkg/apis/correlation/v0alpha1/correlation_status_gen.go b/apps/correlations/pkg/apis/correlation/v0alpha1/correlation_status_gen.go deleted file mode 100644 index 5b8bc9f8088..00000000000 --- a/apps/correlations/pkg/apis/correlation/v0alpha1/correlation_status_gen.go +++ /dev/null @@ -1,44 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v0alpha1 - -// +k8s:openapi-gen=true -type CorrelationstatusOperatorState struct { - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State CorrelationStatusOperatorStateState `json:"state"` - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - // details contains any extra information that is operator-specific - Details map[string]interface{} `json:"details,omitempty"` -} - -// NewCorrelationstatusOperatorState creates a new CorrelationstatusOperatorState object. -func NewCorrelationstatusOperatorState() *CorrelationstatusOperatorState { - return &CorrelationstatusOperatorState{} -} - -// +k8s:openapi-gen=true -type CorrelationStatus struct { - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]CorrelationstatusOperatorState `json:"operatorStates,omitempty"` - // additionalFields is reserved for future use - AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` -} - -// NewCorrelationStatus creates a new CorrelationStatus object. -func NewCorrelationStatus() *CorrelationStatus { - return &CorrelationStatus{} -} - -// +k8s:openapi-gen=true -type CorrelationStatusOperatorStateState string - -const ( - CorrelationStatusOperatorStateStateSuccess CorrelationStatusOperatorStateState = "success" - CorrelationStatusOperatorStateStateInProgress CorrelationStatusOperatorStateState = "in_progress" - CorrelationStatusOperatorStateStateFailed CorrelationStatusOperatorStateState = "failed" -) diff --git a/apps/correlations/pkg/apis/correlation_manifest.go b/apps/correlations/pkg/apis/correlation_manifest.go index 293633bff4f..4ef118d2216 100644 --- a/apps/correlations/pkg/apis/correlation_manifest.go +++ b/apps/correlations/pkg/apis/correlation_manifest.go @@ -10,17 +10,16 @@ import ( "fmt" "strings" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/kube-openapi/pkg/spec3" - "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kube-openapi/pkg/spec3" v0alpha1 "github.com/grafana/grafana/apps/correlations/pkg/apis/correlation/v0alpha1" ) var ( - rawSchemaCorrelationv0alpha1 = []byte(`{"ConfigSpec":{"additionalProperties":false,"description":"there was a deprecated field here called type, we will need to move that for conversion and provisioning","properties":{"field":{"type":"string"},"target":{"$ref":"#/components/schemas/TargetSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationSpec"},"type":"array"}},"required":["field","target"],"type":"object"},"Correlation":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"CorrelationType":{"enum":["query","external"],"type":"string"},"DataSourceRef":{"additionalProperties":false,"properties":{"group":{"description":"same as pluginId","type":"string"},"name":{"description":"same as grafana uid","type":"string"}},"required":["group","name"],"type":"object"},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"TargetSpec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"TransformationSpec":{"additionalProperties":false,"properties":{"expression":{"type":"string"},"field":{"type":"string"},"mapValue":{"type":"string"},"type":{"type":"string"}},"required":["type","expression","field","mapValue"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"config":{"$ref":"#/components/schemas/ConfigSpec"},"description":{"type":"string"},"label":{"type":"string"},"provisioned":{"type":"boolean"},"source_ds_ref":{"$ref":"#/components/schemas/DataSourceRef"},"target_ds_ref":{"$ref":"#/components/schemas/DataSourceRef"},"type":{"$ref":"#/components/schemas/CorrelationType"}},"required":["source_ds_ref","label","config","provisioned","type"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + rawSchemaCorrelationv0alpha1 = []byte(`{"ConfigSpec":{"additionalProperties":false,"description":"there was a deprecated field here called type, we will need to move that for conversion and provisioning","properties":{"field":{"type":"string"},"target":{"$ref":"#/components/schemas/TargetSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationSpec"},"type":"array"}},"required":["field","target"],"type":"object"},"Correlation":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"CorrelationType":{"enum":["query","external"],"type":"string"},"DataSourceRef":{"additionalProperties":false,"properties":{"group":{"description":"same as pluginId","type":"string"},"name":{"description":"same as grafana uid","type":"string"}},"required":["group","name"],"type":"object"},"TargetSpec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"TransformationSpec":{"additionalProperties":false,"properties":{"expression":{"type":"string"},"field":{"type":"string"},"mapValue":{"type":"string"},"type":{"type":"string"}},"required":["type","expression","field","mapValue"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"config":{"$ref":"#/components/schemas/ConfigSpec"},"description":{"type":"string"},"label":{"type":"string"},"provisioned":{"type":"boolean"},"source_ds_ref":{"$ref":"#/components/schemas/DataSourceRef"},"target_ds_ref":{"$ref":"#/components/schemas/DataSourceRef"},"type":{"$ref":"#/components/schemas/CorrelationType"}},"required":["source_ds_ref","label","config","provisioned","type"],"type":"object"}}`) versionSchemaCorrelationv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaCorrelationv0alpha1, &versionSchemaCorrelationv0alpha1) ) diff --git a/apps/correlations/plugin/src/generated/correlation/v0alpha1/correlation_object_gen.ts b/apps/correlations/plugin/src/generated/correlation/v0alpha1/correlation_object_gen.ts index ff5b1b8f03f..05bbaf69516 100644 --- a/apps/correlations/plugin/src/generated/correlation/v0alpha1/correlation_object_gen.ts +++ b/apps/correlations/plugin/src/generated/correlation/v0alpha1/correlation_object_gen.ts @@ -2,7 +2,6 @@ * This file was generated by grafana-app-sdk. DO NOT EDIT. */ import { Spec } from './types.spec.gen'; -import { Status } from './types.status.gen'; export interface Metadata { name: string; @@ -45,5 +44,4 @@ export interface Correlation { apiVersion: string; metadata: Metadata; spec: Spec; - status: Status; } diff --git a/apps/correlations/plugin/src/generated/correlation/v0alpha1/types.status.gen.ts b/apps/correlations/plugin/src/generated/correlation/v0alpha1/types.status.gen.ts deleted file mode 100644 index 01be8df7961..00000000000 --- a/apps/correlations/plugin/src/generated/correlation/v0alpha1/types.status.gen.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -export interface OperatorState { - // lastEvaluation is the ResourceVersion last evaluated - lastEvaluation: string; - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - state: "success" | "in_progress" | "failed"; - // descriptiveState is an optional more descriptive state field which has no requirements on format - descriptiveState?: string; - // details contains any extra information that is operator-specific - details?: Record; -} - -export const defaultOperatorState = (): OperatorState => ({ - lastEvaluation: "", - state: "success", -}); - -export interface Status { - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - operatorStates?: Record; - // additionalFields is reserved for future use - additionalFields?: Record; -} - -export const defaultStatus = (): Status => ({ -}); - diff --git a/apps/iam/Makefile b/apps/iam/Makefile index 71b00aca821..c11c35f7a90 100644 --- a/apps/iam/Makefile +++ b/apps/iam/Makefile @@ -8,6 +8,7 @@ generate: install-app-sdk update-app-sdk ## Run Grafana App SDK code generation --grouping=group \ --defencoding=none \ --noschemasinmanifest \ + --genoperatorstate=false \ --postprocess .PHONY: deps diff --git a/apps/iam/pkg/apis/iam/v0alpha1/corerole_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/corerole_client_gen.go index ae553035b02..371c97fdf4b 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/corerole_client_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/corerole_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type CoreRoleClient struct { @@ -76,24 +75,6 @@ func (c *CoreRoleClient) Patch(ctx context.Context, identifier resource.Identifi return c.client.Patch(ctx, identifier, req, opts) } -func (c *CoreRoleClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus CoreRoleStatus, opts resource.UpdateOptions) (*CoreRole, error) { - return c.client.Update(ctx, &CoreRole{ - TypeMeta: metav1.TypeMeta{ - Kind: CoreRoleKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *CoreRoleClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/corerole_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/corerole_object_gen.go index 4aba32da755..625cced10b9 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/corerole_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/corerole_object_gen.go @@ -21,8 +21,6 @@ type CoreRole struct { // Spec is the spec of the CoreRole Spec CoreRoleSpec `json:"spec" yaml:"spec"` - - Status CoreRoleStatus `json:"status" yaml:"status"` } func (o *CoreRole) GetSpec() any { @@ -39,15 +37,11 @@ func (o *CoreRole) SetSpec(spec any) error { } func (o *CoreRole) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *CoreRole) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *CoreRole) GetSubresource(name string) (any, bool) { func (o *CoreRole) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(CoreRoleStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type CoreRoleStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *CoreRole) DeepCopyInto(dst *CoreRole) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *CoreRoleSpec) DeepCopy() *CoreRoleSpec { func (s *CoreRoleSpec) DeepCopyInto(dst *CoreRoleSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of CoreRoleStatus -func (s *CoreRoleStatus) DeepCopy() *CoreRoleStatus { - cpy := &CoreRoleStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies CoreRoleStatus into another CoreRoleStatus object -func (s *CoreRoleStatus) DeepCopyInto(dst *CoreRoleStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_client_gen.go index 71db46f40fa..1a0133a4798 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_client_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type GlobalRoleClient struct { @@ -76,24 +75,6 @@ func (c *GlobalRoleClient) Patch(ctx context.Context, identifier resource.Identi return c.client.Patch(ctx, identifier, req, opts) } -func (c *GlobalRoleClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus GlobalRoleStatus, opts resource.UpdateOptions) (*GlobalRole, error) { - return c.client.Update(ctx, &GlobalRole{ - TypeMeta: metav1.TypeMeta{ - Kind: GlobalRoleKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *GlobalRoleClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_object_gen.go index 2081adb8507..27165fe70bd 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_object_gen.go @@ -21,8 +21,6 @@ type GlobalRole struct { // Spec is the spec of the GlobalRole Spec GlobalRoleSpec `json:"spec" yaml:"spec"` - - Status GlobalRoleStatus `json:"status" yaml:"status"` } func (o *GlobalRole) GetSpec() any { @@ -39,15 +37,11 @@ func (o *GlobalRole) SetSpec(spec any) error { } func (o *GlobalRole) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *GlobalRole) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *GlobalRole) GetSubresource(name string) (any, bool) { func (o *GlobalRole) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(GlobalRoleStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type GlobalRoleStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *GlobalRole) DeepCopyInto(dst *GlobalRole) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *GlobalRoleSpec) DeepCopy() *GlobalRoleSpec { func (s *GlobalRoleSpec) DeepCopyInto(dst *GlobalRoleSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of GlobalRoleStatus -func (s *GlobalRoleStatus) DeepCopy() *GlobalRoleStatus { - cpy := &GlobalRoleStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies GlobalRoleStatus into another GlobalRoleStatus object -func (s *GlobalRoleStatus) DeepCopyInto(dst *GlobalRoleStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_client_gen.go index 66ea08e4d69..9c06311b938 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_client_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type GlobalRoleBindingClient struct { @@ -76,24 +75,6 @@ func (c *GlobalRoleBindingClient) Patch(ctx context.Context, identifier resource return c.client.Patch(ctx, identifier, req, opts) } -func (c *GlobalRoleBindingClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus GlobalRoleBindingStatus, opts resource.UpdateOptions) (*GlobalRoleBinding, error) { - return c.client.Update(ctx, &GlobalRoleBinding{ - TypeMeta: metav1.TypeMeta{ - Kind: GlobalRoleBindingKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *GlobalRoleBindingClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_object_gen.go index 657f1830ff1..3bd4609d25d 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_object_gen.go @@ -21,8 +21,6 @@ type GlobalRoleBinding struct { // Spec is the spec of the GlobalRoleBinding Spec GlobalRoleBindingSpec `json:"spec" yaml:"spec"` - - Status GlobalRoleBindingStatus `json:"status" yaml:"status"` } func (o *GlobalRoleBinding) GetSpec() any { @@ -39,15 +37,11 @@ func (o *GlobalRoleBinding) SetSpec(spec any) error { } func (o *GlobalRoleBinding) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *GlobalRoleBinding) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *GlobalRoleBinding) GetSubresource(name string) (any, bool) { func (o *GlobalRoleBinding) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(GlobalRoleBindingStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type GlobalRoleBindingStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *GlobalRoleBinding) DeepCopyInto(dst *GlobalRoleBinding) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *GlobalRoleBindingSpec) DeepCopy() *GlobalRoleBindingSpec { func (s *GlobalRoleBindingSpec) DeepCopyInto(dst *GlobalRoleBindingSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of GlobalRoleBindingStatus -func (s *GlobalRoleBindingStatus) DeepCopy() *GlobalRoleBindingStatus { - cpy := &GlobalRoleBindingStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies GlobalRoleBindingStatus into another GlobalRoleBindingStatus object -func (s *GlobalRoleBindingStatus) DeepCopyInto(dst *GlobalRoleBindingStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_client_gen.go index 05c70845e26..dab46ffdc05 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_client_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type ResourcePermissionClient struct { @@ -76,24 +75,6 @@ func (c *ResourcePermissionClient) Patch(ctx context.Context, identifier resourc return c.client.Patch(ctx, identifier, req, opts) } -func (c *ResourcePermissionClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus ResourcePermissionStatus, opts resource.UpdateOptions) (*ResourcePermission, error) { - return c.client.Update(ctx, &ResourcePermission{ - TypeMeta: metav1.TypeMeta{ - Kind: ResourcePermissionKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *ResourcePermissionClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_object_gen.go index 4fcebde9e01..996beb7e002 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_object_gen.go @@ -21,8 +21,6 @@ type ResourcePermission struct { // Spec is the spec of the ResourcePermission Spec ResourcePermissionSpec `json:"spec" yaml:"spec"` - - Status ResourcePermissionStatus `json:"status" yaml:"status"` } func (o *ResourcePermission) GetSpec() any { @@ -39,15 +37,11 @@ func (o *ResourcePermission) SetSpec(spec any) error { } func (o *ResourcePermission) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *ResourcePermission) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *ResourcePermission) GetSubresource(name string) (any, bool) { func (o *ResourcePermission) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(ResourcePermissionStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type ResourcePermissionStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *ResourcePermission) DeepCopyInto(dst *ResourcePermission) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *ResourcePermissionSpec) DeepCopy() *ResourcePermissionSpec { func (s *ResourcePermissionSpec) DeepCopyInto(dst *ResourcePermissionSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of ResourcePermissionStatus -func (s *ResourcePermissionStatus) DeepCopy() *ResourcePermissionStatus { - cpy := &ResourcePermissionStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies ResourcePermissionStatus into another ResourcePermissionStatus object -func (s *ResourcePermissionStatus) DeepCopyInto(dst *ResourcePermissionStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/role_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/role_client_gen.go index e61d3fc83dd..b53bfdd9e6d 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/role_client_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/role_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type RoleClient struct { @@ -76,24 +75,6 @@ func (c *RoleClient) Patch(ctx context.Context, identifier resource.Identifier, return c.client.Patch(ctx, identifier, req, opts) } -func (c *RoleClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus RoleStatus, opts resource.UpdateOptions) (*Role, error) { - return c.client.Update(ctx, &Role{ - TypeMeta: metav1.TypeMeta{ - Kind: RoleKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *RoleClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/role_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/role_object_gen.go index 9256a618c9f..20bb587157e 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/role_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/role_object_gen.go @@ -21,8 +21,6 @@ type Role struct { // Spec is the spec of the Role Spec RoleSpec `json:"spec" yaml:"spec"` - - Status RoleStatus `json:"status" yaml:"status"` } func (o *Role) GetSpec() any { @@ -39,15 +37,11 @@ func (o *Role) SetSpec(spec any) error { } func (o *Role) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *Role) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *Role) GetSubresource(name string) (any, bool) { func (o *Role) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(RoleStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type RoleStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *Role) DeepCopyInto(dst *Role) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *RoleSpec) DeepCopy() *RoleSpec { func (s *RoleSpec) DeepCopyInto(dst *RoleSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of RoleStatus -func (s *RoleStatus) DeepCopy() *RoleStatus { - cpy := &RoleStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies RoleStatus into another RoleStatus object -func (s *RoleStatus) DeepCopyInto(dst *RoleStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_client_gen.go index 13e78c70adf..96807b1b795 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_client_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type RoleBindingClient struct { @@ -76,24 +75,6 @@ func (c *RoleBindingClient) Patch(ctx context.Context, identifier resource.Ident return c.client.Patch(ctx, identifier, req, opts) } -func (c *RoleBindingClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus RoleBindingStatus, opts resource.UpdateOptions) (*RoleBinding, error) { - return c.client.Update(ctx, &RoleBinding{ - TypeMeta: metav1.TypeMeta{ - Kind: RoleBindingKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *RoleBindingClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_object_gen.go index bec75890d89..dfd7741e05a 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_object_gen.go @@ -21,8 +21,6 @@ type RoleBinding struct { // Spec is the spec of the RoleBinding Spec RoleBindingSpec `json:"spec" yaml:"spec"` - - Status RoleBindingStatus `json:"status" yaml:"status"` } func (o *RoleBinding) GetSpec() any { @@ -39,15 +37,11 @@ func (o *RoleBinding) SetSpec(spec any) error { } func (o *RoleBinding) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *RoleBinding) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *RoleBinding) GetSubresource(name string) (any, bool) { func (o *RoleBinding) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(RoleBindingStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type RoleBindingStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *RoleBinding) DeepCopyInto(dst *RoleBinding) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *RoleBindingSpec) DeepCopy() *RoleBindingSpec { func (s *RoleBindingSpec) DeepCopyInto(dst *RoleBindingSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of RoleBindingStatus -func (s *RoleBindingStatus) DeepCopy() *RoleBindingStatus { - cpy := &RoleBindingStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies RoleBindingStatus into another RoleBindingStatus object -func (s *RoleBindingStatus) DeepCopyInto(dst *RoleBindingStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_client_gen.go index 3b922e8b313..0ce1616daa3 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_client_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type ServiceAccountClient struct { @@ -76,24 +75,6 @@ func (c *ServiceAccountClient) Patch(ctx context.Context, identifier resource.Id return c.client.Patch(ctx, identifier, req, opts) } -func (c *ServiceAccountClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus ServiceAccountStatus, opts resource.UpdateOptions) (*ServiceAccount, error) { - return c.client.Update(ctx, &ServiceAccount{ - TypeMeta: metav1.TypeMeta{ - Kind: ServiceAccountKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *ServiceAccountClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go index 081e244dd6e..fe3cd7f3609 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go @@ -21,8 +21,6 @@ type ServiceAccount struct { // Spec is the spec of the ServiceAccount Spec ServiceAccountSpec `json:"spec" yaml:"spec"` - - Status ServiceAccountStatus `json:"status" yaml:"status"` } func (o *ServiceAccount) GetSpec() any { @@ -39,15 +37,11 @@ func (o *ServiceAccount) SetSpec(spec any) error { } func (o *ServiceAccount) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *ServiceAccount) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *ServiceAccount) GetSubresource(name string) (any, bool) { func (o *ServiceAccount) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(ServiceAccountStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type ServiceAccountStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *ServiceAccount) DeepCopyInto(dst *ServiceAccount) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *ServiceAccountSpec) DeepCopy() *ServiceAccountSpec { func (s *ServiceAccountSpec) DeepCopyInto(dst *ServiceAccountSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of ServiceAccountStatus -func (s *ServiceAccountStatus) DeepCopy() *ServiceAccountStatus { - cpy := &ServiceAccountStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies ServiceAccountStatus into another ServiceAccountStatus object -func (s *ServiceAccountStatus) DeepCopyInto(dst *ServiceAccountStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_client_gen.go index c166c6c2881..807c68dc0f6 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/team_client_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type TeamClient struct { @@ -76,24 +75,6 @@ func (c *TeamClient) Patch(ctx context.Context, identifier resource.Identifier, return c.client.Patch(ctx, identifier, req, opts) } -func (c *TeamClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus TeamStatus, opts resource.UpdateOptions) (*Team, error) { - return c.client.Update(ctx, &Team{ - TypeMeta: metav1.TypeMeta{ - Kind: TeamKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *TeamClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go index bfc949acfd1..4030bebb9d1 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go @@ -21,8 +21,6 @@ type Team struct { // Spec is the spec of the Team Spec TeamSpec `json:"spec" yaml:"spec"` - - Status TeamStatus `json:"status" yaml:"status"` } func (o *Team) GetSpec() any { @@ -39,15 +37,11 @@ func (o *Team) SetSpec(spec any) error { } func (o *Team) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *Team) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *Team) GetSubresource(name string) (any, bool) { func (o *Team) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(TeamStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type TeamStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *Team) DeepCopyInto(dst *Team) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *TeamSpec) DeepCopy() *TeamSpec { func (s *TeamSpec) DeepCopyInto(dst *TeamSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of TeamStatus -func (s *TeamStatus) DeepCopy() *TeamStatus { - cpy := &TeamStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies TeamStatus into another TeamStatus object -func (s *TeamStatus) DeepCopyInto(dst *TeamStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_client_gen.go index 39729e1ece1..faaf86c1b00 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_client_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type TeamBindingClient struct { @@ -76,24 +75,6 @@ func (c *TeamBindingClient) Patch(ctx context.Context, identifier resource.Ident return c.client.Patch(ctx, identifier, req, opts) } -func (c *TeamBindingClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus TeamBindingStatus, opts resource.UpdateOptions) (*TeamBinding, error) { - return c.client.Update(ctx, &TeamBinding{ - TypeMeta: metav1.TypeMeta{ - Kind: TeamBindingKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *TeamBindingClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go index 6c9f6f407e3..a958c55f5e7 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go @@ -21,8 +21,6 @@ type TeamBinding struct { // Spec is the spec of the TeamBinding Spec TeamBindingSpec `json:"spec" yaml:"spec"` - - Status TeamBindingStatus `json:"status" yaml:"status"` } func (o *TeamBinding) GetSpec() any { @@ -39,15 +37,11 @@ func (o *TeamBinding) SetSpec(spec any) error { } func (o *TeamBinding) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *TeamBinding) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *TeamBinding) GetSubresource(name string) (any, bool) { func (o *TeamBinding) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(TeamBindingStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type TeamBindingStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *TeamBinding) DeepCopyInto(dst *TeamBinding) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *TeamBindingSpec) DeepCopy() *TeamBindingSpec { func (s *TeamBindingSpec) DeepCopyInto(dst *TeamBindingSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of TeamBindingStatus -func (s *TeamBindingStatus) DeepCopy() *TeamBindingStatus { - cpy := &TeamBindingStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies TeamBindingStatus into another TeamBindingStatus object -func (s *TeamBindingStatus) DeepCopyInto(dst *TeamBindingStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go index bd7af9b3361..665df84327e 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type UserClient struct { @@ -76,24 +75,6 @@ func (c *UserClient) Patch(ctx context.Context, identifier resource.Identifier, return c.client.Patch(ctx, identifier, req, opts) } -func (c *UserClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus UserStatus, opts resource.UpdateOptions) (*User, error) { - return c.client.Update(ctx, &User{ - TypeMeta: metav1.TypeMeta{ - Kind: UserKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *UserClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go index ce2defa33a8..373112a1d87 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go @@ -21,8 +21,6 @@ type User struct { // Spec is the spec of the User Spec UserSpec `json:"spec" yaml:"spec"` - - Status UserStatus `json:"status" yaml:"status"` } func (o *User) GetSpec() any { @@ -39,15 +37,11 @@ func (o *User) SetSpec(spec any) error { } func (o *User) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *User) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *User) GetSubresource(name string) (any, bool) { func (o *User) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(UserStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type UserStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *User) DeepCopyInto(dst *User) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *UserSpec) DeepCopy() *UserSpec { func (s *UserSpec) DeepCopyInto(dst *UserSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of UserStatus -func (s *UserStatus) DeepCopy() *UserStatus { - cpy := &UserStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies UserStatus into another UserStatus object -func (s *UserStatus) DeepCopyInto(dst *UserStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go index fb6892fa09d..cb2e31f9144 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go @@ -109,18 +109,12 @@ func schema_pkg_apis_iam_v0alpha1_CoreRole(ref common.ReferenceCallback) common. Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleSpec"), }, }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleStatus"), - }, - }, }, - Required: []string{"metadata", "spec", "status"}, + Required: []string{"metadata", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.CoreRoleSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } @@ -387,18 +381,12 @@ func schema_pkg_apis_iam_v0alpha1_GlobalRole(ref common.ReferenceCallback) commo Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleSpec"), }, }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleStatus"), - }, - }, }, - Required: []string{"metadata", "spec", "status"}, + Required: []string{"metadata", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } @@ -435,18 +423,12 @@ func schema_pkg_apis_iam_v0alpha1_GlobalRoleBinding(ref common.ReferenceCallback Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingSpec"), }, }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingStatus"), - }, - }, }, - Required: []string{"metadata", "spec", "status"}, + Required: []string{"metadata", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.GlobalRoleBindingSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } @@ -948,18 +930,12 @@ func schema_pkg_apis_iam_v0alpha1_ResourcePermission(ref common.ReferenceCallbac Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionSpec"), }, }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionStatus"), - }, - }, }, - Required: []string{"metadata", "spec", "status"}, + Required: []string{"metadata", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ResourcePermissionSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } @@ -1247,18 +1223,12 @@ func schema_pkg_apis_iam_v0alpha1_Role(ref common.ReferenceCallback) common.Open Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleSpec"), }, }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleStatus"), - }, - }, }, - Required: []string{"metadata", "spec", "status"}, + Required: []string{"metadata", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } @@ -1295,18 +1265,12 @@ func schema_pkg_apis_iam_v0alpha1_RoleBinding(ref common.ReferenceCallback) comm Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingSpec"), }, }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingStatus"), - }, - }, }, - Required: []string{"metadata", "spec", "status"}, + Required: []string{"metadata", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.RoleBindingSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } @@ -1808,18 +1772,12 @@ func schema_pkg_apis_iam_v0alpha1_ServiceAccount(ref common.ReferenceCallback) c Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountSpec"), }, }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountStatus"), - }, - }, }, - Required: []string{"metadata", "spec", "status"}, + Required: []string{"metadata", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.ServiceAccountSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } @@ -2040,18 +1998,12 @@ func schema_pkg_apis_iam_v0alpha1_Team(ref common.ReferenceCallback) common.Open Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec"), }, }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus"), - }, - }, }, - Required: []string{"metadata", "spec", "status"}, + Required: []string{"metadata", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } @@ -2088,18 +2040,12 @@ func schema_pkg_apis_iam_v0alpha1_TeamBinding(ref common.ReferenceCallback) comm Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingSpec"), }, }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingStatus"), - }, - }, }, - Required: []string{"metadata", "spec", "status"}, + Required: []string{"metadata", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamBindingSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } @@ -2547,18 +2493,12 @@ func schema_pkg_apis_iam_v0alpha1_User(ref common.ReferenceCallback) common.Open Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec"), }, }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus"), - }, - }, }, - Required: []string{"metadata", "spec", "status"}, + Required: []string{"metadata", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec", "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.UserSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } diff --git a/apps/preferences/Makefile b/apps/preferences/Makefile index 230bfd4149a..bc8d6d30cb5 100644 --- a/apps/preferences/Makefile +++ b/apps/preferences/Makefile @@ -6,4 +6,5 @@ generate: install-app-sdk update-app-sdk --source=./kinds/ \ --gogenpath=./pkg/apis \ --grouping=group \ + --genoperatorstate=false \ --defencoding=none \ No newline at end of file diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/preferences_client_gen.go b/apps/preferences/pkg/apis/preferences/v1alpha1/preferences_client_gen.go index 380bafb16bf..66e661e574d 100644 --- a/apps/preferences/pkg/apis/preferences/v1alpha1/preferences_client_gen.go +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/preferences_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type PreferencesClient struct { @@ -76,24 +75,6 @@ func (c *PreferencesClient) Patch(ctx context.Context, identifier resource.Ident return c.client.Patch(ctx, identifier, req, opts) } -func (c *PreferencesClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus PreferencesStatus, opts resource.UpdateOptions) (*Preferences, error) { - return c.client.Update(ctx, &Preferences{ - TypeMeta: metav1.TypeMeta{ - Kind: PreferencesKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *PreferencesClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/preferences_object_gen.go b/apps/preferences/pkg/apis/preferences/v1alpha1/preferences_object_gen.go index 9f1857a5f7b..81a4b4351ab 100644 --- a/apps/preferences/pkg/apis/preferences/v1alpha1/preferences_object_gen.go +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/preferences_object_gen.go @@ -21,8 +21,6 @@ type Preferences struct { // Spec is the spec of the Preferences Spec PreferencesSpec `json:"spec" yaml:"spec"` - - Status PreferencesStatus `json:"status" yaml:"status"` } func (o *Preferences) GetSpec() any { @@ -39,15 +37,11 @@ func (o *Preferences) SetSpec(spec any) error { } func (o *Preferences) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *Preferences) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *Preferences) GetSubresource(name string) (any, bool) { func (o *Preferences) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(PreferencesStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type PreferencesStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *Preferences) DeepCopyInto(dst *Preferences) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *PreferencesSpec) DeepCopy() *PreferencesSpec { func (s *PreferencesSpec) DeepCopyInto(dst *PreferencesSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of PreferencesStatus -func (s *PreferencesStatus) DeepCopy() *PreferencesStatus { - cpy := &PreferencesStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies PreferencesStatus into another PreferencesStatus object -func (s *PreferencesStatus) DeepCopyInto(dst *PreferencesStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/preferences_status_gen.go b/apps/preferences/pkg/apis/preferences/v1alpha1/preferences_status_gen.go deleted file mode 100644 index 420dd054d3d..00000000000 --- a/apps/preferences/pkg/apis/preferences/v1alpha1/preferences_status_gen.go +++ /dev/null @@ -1,44 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v1alpha1 - -// +k8s:openapi-gen=true -type PreferencesstatusOperatorState struct { - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State PreferencesStatusOperatorStateState `json:"state"` - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - // details contains any extra information that is operator-specific - Details map[string]interface{} `json:"details,omitempty"` -} - -// NewPreferencesstatusOperatorState creates a new PreferencesstatusOperatorState object. -func NewPreferencesstatusOperatorState() *PreferencesstatusOperatorState { - return &PreferencesstatusOperatorState{} -} - -// +k8s:openapi-gen=true -type PreferencesStatus struct { - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]PreferencesstatusOperatorState `json:"operatorStates,omitempty"` - // additionalFields is reserved for future use - AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` -} - -// NewPreferencesStatus creates a new PreferencesStatus object. -func NewPreferencesStatus() *PreferencesStatus { - return &PreferencesStatus{} -} - -// +k8s:openapi-gen=true -type PreferencesStatusOperatorStateState string - -const ( - PreferencesStatusOperatorStateStateSuccess PreferencesStatusOperatorStateState = "success" - PreferencesStatusOperatorStateStateInProgress PreferencesStatusOperatorStateState = "in_progress" - PreferencesStatusOperatorStateStateFailed PreferencesStatusOperatorStateState = "failed" -) diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_client_gen.go b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_client_gen.go index 3a607012db3..9f2c56ea59b 100644 --- a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_client_gen.go +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_client_gen.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana-app-sdk/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type StarsClient struct { @@ -76,24 +75,6 @@ func (c *StarsClient) Patch(ctx context.Context, identifier resource.Identifier, return c.client.Patch(ctx, identifier, req, opts) } -func (c *StarsClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus StarsStatus, opts resource.UpdateOptions) (*Stars, error) { - return c.client.Update(ctx, &Stars{ - TypeMeta: metav1.TypeMeta{ - Kind: StarsKind().Kind(), - APIVersion: GroupVersion.Identifier(), - }, - ObjectMeta: metav1.ObjectMeta{ - ResourceVersion: opts.ResourceVersion, - Namespace: identifier.Namespace, - Name: identifier.Name, - }, - Status: newStatus, - }, resource.UpdateOptions{ - Subresource: "status", - ResourceVersion: opts.ResourceVersion, - }) -} - func (c *StarsClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_object_gen.go b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_object_gen.go index d6836cea565..8381844dceb 100644 --- a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_object_gen.go +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_object_gen.go @@ -21,8 +21,6 @@ type Stars struct { // Spec is the spec of the Stars Spec StarsSpec `json:"spec" yaml:"spec"` - - Status StarsStatus `json:"status" yaml:"status"` } func (o *Stars) GetSpec() any { @@ -39,15 +37,11 @@ func (o *Stars) SetSpec(spec any) error { } func (o *Stars) GetSubresources() map[string]any { - return map[string]any{ - "status": o.Status, - } + return map[string]any{} } func (o *Stars) GetSubresource(name string) (any, bool) { switch name { - case "status": - return o.Status, true default: return nil, false } @@ -55,13 +49,6 @@ func (o *Stars) GetSubresource(name string) (any, bool) { func (o *Stars) SetSubresource(name string, value any) error { switch name { - case "status": - cast, ok := value.(StarsStatus) - if !ok { - return fmt.Errorf("cannot set status type %#v, not of type StarsStatus", value) - } - o.Status = cast - return nil default: return fmt.Errorf("subresource '%s' does not exist", name) } @@ -233,7 +220,6 @@ func (o *Stars) DeepCopyInto(dst *Stars) { dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) o.Spec.DeepCopyInto(&dst.Spec) - o.Status.DeepCopyInto(&dst.Status) } // Interface compliance compile-time check @@ -305,15 +291,3 @@ func (s *StarsSpec) DeepCopy() *StarsSpec { func (s *StarsSpec) DeepCopyInto(dst *StarsSpec) { resource.CopyObjectInto(dst, s) } - -// DeepCopy creates a full deep copy of StarsStatus -func (s *StarsStatus) DeepCopy() *StarsStatus { - cpy := &StarsStatus{} - s.DeepCopyInto(cpy) - return cpy -} - -// DeepCopyInto deep copies StarsStatus into another StarsStatus object -func (s *StarsStatus) DeepCopyInto(dst *StarsStatus) { - resource.CopyObjectInto(dst, s) -} diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_status_gen.go b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_status_gen.go deleted file mode 100644 index c8c7c2b07f2..00000000000 --- a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_status_gen.go +++ /dev/null @@ -1,44 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v1alpha1 - -// +k8s:openapi-gen=true -type StarsstatusOperatorState struct { - // lastEvaluation is the ResourceVersion last evaluated - LastEvaluation string `json:"lastEvaluation"` - // state describes the state of the lastEvaluation. - // It is limited to three possible states for machine evaluation. - State StarsStatusOperatorStateState `json:"state"` - // descriptiveState is an optional more descriptive state field which has no requirements on format - DescriptiveState *string `json:"descriptiveState,omitempty"` - // details contains any extra information that is operator-specific - Details map[string]interface{} `json:"details,omitempty"` -} - -// NewStarsstatusOperatorState creates a new StarsstatusOperatorState object. -func NewStarsstatusOperatorState() *StarsstatusOperatorState { - return &StarsstatusOperatorState{} -} - -// +k8s:openapi-gen=true -type StarsStatus struct { - // operatorStates is a map of operator ID to operator state evaluations. - // Any operator which consumes this kind SHOULD add its state evaluation information to this field. - OperatorStates map[string]StarsstatusOperatorState `json:"operatorStates,omitempty"` - // additionalFields is reserved for future use - AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` -} - -// NewStarsStatus creates a new StarsStatus object. -func NewStarsStatus() *StarsStatus { - return &StarsStatus{} -} - -// +k8s:openapi-gen=true -type StarsStatusOperatorStateState string - -const ( - StarsStatusOperatorStateStateSuccess StarsStatusOperatorStateState = "success" - StarsStatusOperatorStateStateInProgress StarsStatusOperatorStateState = "in_progress" - StarsStatusOperatorStateStateFailed StarsStatusOperatorStateState = "failed" -) diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi.go b/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi.go index 7b9014a57bb..c8700a3dfa5 100644 --- a/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi.go +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi.go @@ -20,14 +20,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesNavbarPreference": schema_pkg_apis_preferences_v1alpha1_PreferencesNavbarPreference(ref), "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesQueryHistoryPreference": schema_pkg_apis_preferences_v1alpha1_PreferencesQueryHistoryPreference(ref), "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec": schema_pkg_apis_preferences_v1alpha1_PreferencesSpec(ref), - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesStatus": schema_pkg_apis_preferences_v1alpha1_PreferencesStatus(ref), - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesstatusOperatorState": schema_pkg_apis_preferences_v1alpha1_PreferencesstatusOperatorState(ref), "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Stars": schema_pkg_apis_preferences_v1alpha1_Stars(ref), "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsList": schema_pkg_apis_preferences_v1alpha1_StarsList(ref), "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsResource": schema_pkg_apis_preferences_v1alpha1_StarsResource(ref), "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec": schema_pkg_apis_preferences_v1alpha1_StarsSpec(ref), - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsStatus": schema_pkg_apis_preferences_v1alpha1_StarsStatus(ref), - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsstatusOperatorState": schema_pkg_apis_preferences_v1alpha1_StarsstatusOperatorState(ref), } } @@ -64,18 +60,12 @@ func schema_pkg_apis_preferences_v1alpha1_Preferences(ref common.ReferenceCallba Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec"), }, }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesStatus"), - }, - }, }, - Required: []string{"metadata", "spec", "status"}, + Required: []string{"metadata", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } @@ -277,101 +267,6 @@ func schema_pkg_apis_preferences_v1alpha1_PreferencesSpec(ref common.ReferenceCa } } -func schema_pkg_apis_preferences_v1alpha1_PreferencesStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "operatorStates": { - SchemaProps: spec.SchemaProps{ - Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesstatusOperatorState"), - }, - }, - }, - }, - }, - "additionalFields": { - SchemaProps: spec.SchemaProps{ - Description: "additionalFields is reserved for future use", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesstatusOperatorState"}, - } -} - -func schema_pkg_apis_preferences_v1alpha1_PreferencesstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "lastEvaluation": { - SchemaProps: spec.SchemaProps{ - Description: "lastEvaluation is the ResourceVersion last evaluated", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "state": { - SchemaProps: spec.SchemaProps{ - Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "descriptiveState": { - SchemaProps: spec.SchemaProps{ - Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", - Type: []string{"string"}, - Format: "", - }, - }, - "details": { - SchemaProps: spec.SchemaProps{ - Description: "details contains any extra information that is operator-specific", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - Required: []string{"lastEvaluation", "state"}, - }, - }, - } -} - func schema_pkg_apis_preferences_v1alpha1_Stars(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -405,18 +300,12 @@ func schema_pkg_apis_preferences_v1alpha1_Stars(ref common.ReferenceCallback) co Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec"), }, }, - "status": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsStatus"), - }, - }, }, - Required: []string{"metadata", "spec", "status"}, + Required: []string{"metadata", "spec"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } @@ -542,98 +431,3 @@ func schema_pkg_apis_preferences_v1alpha1_StarsSpec(ref common.ReferenceCallback "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsResource"}, } } - -func schema_pkg_apis_preferences_v1alpha1_StarsStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "operatorStates": { - SchemaProps: spec.SchemaProps{ - Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsstatusOperatorState"), - }, - }, - }, - }, - }, - "additionalFields": { - SchemaProps: spec.SchemaProps{ - Description: "additionalFields is reserved for future use", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsstatusOperatorState"}, - } -} - -func schema_pkg_apis_preferences_v1alpha1_StarsstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "lastEvaluation": { - SchemaProps: spec.SchemaProps{ - Description: "lastEvaluation is the ResourceVersion last evaluated", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "state": { - SchemaProps: spec.SchemaProps{ - Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "descriptiveState": { - SchemaProps: spec.SchemaProps{ - Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", - Type: []string{"string"}, - Format: "", - }, - }, - "details": { - SchemaProps: spec.SchemaProps{ - Description: "details contains any extra information that is operator-specific", - Type: []string{"object"}, - AdditionalProperties: &spec.SchemaOrBool{ - Allows: true, - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Format: "", - }, - }, - }, - }, - }, - }, - Required: []string{"lastEvaluation", "state"}, - }, - }, - } -} diff --git a/apps/preferences/pkg/apis/preferences_manifest.go b/apps/preferences/pkg/apis/preferences_manifest.go index b0a3468058a..3e9e865df6d 100644 --- a/apps/preferences/pkg/apis/preferences_manifest.go +++ b/apps/preferences/pkg/apis/preferences_manifest.go @@ -19,10 +19,10 @@ import ( ) var ( - rawSchemaPreferencesv1alpha1 = []byte(`{"CookiePreferences":{"additionalProperties":false,"properties":{"analytics":{"additionalProperties":{},"type":"object"},"functional":{"additionalProperties":{},"type":"object"},"performance":{"additionalProperties":{},"type":"object"}},"type":"object"},"NavbarPreference":{"additionalProperties":false,"properties":{"bookmarkUrls":{"items":{"type":"string"},"type":"array"}},"required":["bookmarkUrls"],"type":"object"},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Preferences":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"QueryHistoryPreference":{"additionalProperties":false,"properties":{"homeTab":{"description":"one of: '' | 'query' | 'starred';","type":"string"}},"type":"object"},"spec":{"additionalProperties":false,"properties":{"cookiePreferences":{"$ref":"#/components/schemas/CookiePreferences","description":"Cookie preferences"},"homeDashboardUID":{"description":"UID for the home dashboard","type":"string"},"language":{"description":"Selected language (beta)","type":"string"},"navbar":{"$ref":"#/components/schemas/NavbarPreference","description":"Navigation preferences"},"queryHistory":{"$ref":"#/components/schemas/QueryHistoryPreference","description":"Explore query history preferences"},"regionalFormat":{"description":"Selected locale (beta)","type":"string"},"theme":{"description":"light, dark, empty is default","type":"string"},"timezone":{"description":"The timezone selection\nTODO: this should use the timezone defined in common","type":"string"},"weekStart":{"description":"day of the week (sunday, monday, etc)","type":"string"}},"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + rawSchemaPreferencesv1alpha1 = []byte(`{"CookiePreferences":{"additionalProperties":false,"properties":{"analytics":{"additionalProperties":{},"type":"object"},"functional":{"additionalProperties":{},"type":"object"},"performance":{"additionalProperties":{},"type":"object"}},"type":"object"},"NavbarPreference":{"additionalProperties":false,"properties":{"bookmarkUrls":{"items":{"type":"string"},"type":"array"}},"required":["bookmarkUrls"],"type":"object"},"Preferences":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"QueryHistoryPreference":{"additionalProperties":false,"properties":{"homeTab":{"description":"one of: '' | 'query' | 'starred';","type":"string"}},"type":"object"},"spec":{"additionalProperties":false,"properties":{"cookiePreferences":{"$ref":"#/components/schemas/CookiePreferences","description":"Cookie preferences"},"homeDashboardUID":{"description":"UID for the home dashboard","type":"string"},"language":{"description":"Selected language (beta)","type":"string"},"navbar":{"$ref":"#/components/schemas/NavbarPreference","description":"Navigation preferences"},"queryHistory":{"$ref":"#/components/schemas/QueryHistoryPreference","description":"Explore query history preferences"},"regionalFormat":{"description":"Selected locale (beta)","type":"string"},"theme":{"description":"light, dark, empty is default","type":"string"},"timezone":{"description":"The timezone selection\nTODO: this should use the timezone defined in common","type":"string"},"weekStart":{"description":"day of the week (sunday, monday, etc)","type":"string"}},"type":"object"}}`) versionSchemaPreferencesv1alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaPreferencesv1alpha1, &versionSchemaPreferencesv1alpha1) - rawSchemaStarsv1alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Resource":{"additionalProperties":false,"properties":{"group":{"type":"string"},"kind":{"type":"string"},"names":{"description":"The set of resources\n+listType=set","items":{"type":"string"},"type":"array"}},"required":["group","kind","names"],"type":"object"},"Stars":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"resource":{"items":{"$ref":"#/components/schemas/Resource"},"type":"array"}},"required":["resource"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + rawSchemaStarsv1alpha1 = []byte(`{"Resource":{"additionalProperties":false,"properties":{"group":{"type":"string"},"kind":{"type":"string"},"names":{"description":"The set of resources\n+listType=set","items":{"type":"string"},"type":"array"}},"required":["group","kind","names"],"type":"object"},"Stars":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"resource":{"items":{"$ref":"#/components/schemas/Resource"},"type":"array"}},"required":["resource"],"type":"object"}}`) versionSchemaStarsv1alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaStarsv1alpha1, &versionSchemaStarsv1alpha1) ) diff --git a/apps/provisioning/Makefile b/apps/provisioning/Makefile index c3d7e748603..e9a8b1fbdf9 100644 --- a/apps/provisioning/Makefile +++ b/apps/provisioning/Makefile @@ -1,8 +1,13 @@ include ../sdk.mk -.PHONY: generate +.PHONY: generate # Run Grafana App SDK code generation generate: install-app-sdk update-app-sdk - @$(APP_SDK_BIN) generate -g ./kinds --grouping=group --postprocess --defencoding=none --useoldmanifestkinds + @$(APP_SDK_BIN) generate \ + --source=./kinds/ \ + --gogenpath=./pkg/apis \ + --grouping=group \ + --genoperatorstate=false \ + --defencoding=none .PHONY: build build: generate diff --git a/pkg/tests/apis/iam/iam_test.go b/pkg/tests/apis/iam/iam_test.go index 5ad2eccd8f4..ffd7dedf77c 100644 --- a/pkg/tests/apis/iam/iam_test.go +++ b/pkg/tests/apis/iam/iam_test.go @@ -69,8 +69,7 @@ func TestIntegrationIdentity(t *testing.T) { "title": "staff", "provisioned": false, "externalUID": "" - }, - "status": {} + } } ] }`, found) diff --git a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json index a1d5158e140..9a7b8ef0481 100644 --- a/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json @@ -3502,8 +3502,7 @@ "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -3530,14 +3529,6 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountSpec" } ] - }, - "status": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountStatus" - } - ] } }, "x-kubernetes-group-version-kind": [ @@ -3618,66 +3609,11 @@ } } }, - "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountstatusOperatorState" - } - ] - } - } - } - }, - "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.ServiceAccountstatusOperatorState": { - "type": "object", - "required": [ - "lastEvaluation", - "state" - ], - "properties": { - "descriptiveState": { - "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", - "type": "string" - }, - "details": { - "description": "details contains any extra information that is operator-specific", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "lastEvaluation": { - "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string", - "default": "" - }, - "state": { - "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - "type": "string", - "default": "" - } - } - }, "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.Team": { "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -3704,14 +3640,6 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamSpec" } ] - }, - "status": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamStatus" - } - ] } }, "x-kubernetes-group-version-kind": [ @@ -3726,8 +3654,7 @@ "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -3754,14 +3681,6 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingSpec" } ] - }, - "status": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingStatus" - } - ] } }, "x-kubernetes-group-version-kind": [ @@ -3843,30 +3762,6 @@ } } }, - "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingstatusOperatorState" - } - ] - } - } - } - }, "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingTeamRef": { "type": "object", "required": [ @@ -3899,36 +3794,6 @@ } } }, - "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamBindingstatusOperatorState": { - "type": "object", - "required": [ - "lastEvaluation", - "state" - ], - "properties": { - "descriptiveState": { - "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", - "type": "string" - }, - "details": { - "description": "details contains any extra information that is operator-specific", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "lastEvaluation": { - "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string", - "default": "" - }, - "state": { - "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - "type": "string", - "default": "" - } - } - }, "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamList": { "type": "object", "required": [ @@ -3999,66 +3864,11 @@ } } }, - "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamstatusOperatorState" - } - ] - } - } - } - }, - "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.TeamstatusOperatorState": { - "type": "object", - "required": [ - "lastEvaluation", - "state" - ], - "properties": { - "descriptiveState": { - "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", - "type": "string" - }, - "details": { - "description": "details contains any extra information that is operator-specific", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "lastEvaluation": { - "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string", - "default": "" - }, - "state": { - "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - "type": "string", - "default": "" - } - } - }, "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.User": { "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -4085,14 +3895,6 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserSpec" } ] - }, - "status": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserStatus" - } - ] } }, "x-kubernetes-group-version-kind": [ @@ -4193,60 +3995,6 @@ } } }, - "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserstatusOperatorState" - } - ] - } - } - } - }, - "com.github.grafana.grafana.apps.iam.pkg.apis.iam.v0alpha1.UserstatusOperatorState": { - "type": "object", - "required": [ - "lastEvaluation", - "state" - ], - "properties": { - "descriptiveState": { - "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", - "type": "string" - }, - "details": { - "description": "details contains any extra information that is operator-specific", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "lastEvaluation": { - "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string", - "default": "" - }, - "state": { - "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - "type": "string", - "default": "" - } - } - }, "com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured": { "type": "object", "additionalProperties": true, @@ -4705,8 +4453,7 @@ "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -4723,9 +4470,6 @@ "spec": { "description": "Spec is the spec of the CoreRole", "default": {} - }, - "status": { - "default": {} } } }, @@ -4864,8 +4608,7 @@ "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -4882,9 +4625,6 @@ "spec": { "description": "Spec is the spec of the GlobalRole", "default": {} - }, - "status": { - "default": {} } } }, @@ -4892,8 +4632,7 @@ "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -4910,9 +4649,6 @@ "spec": { "description": "Spec is the spec of the GlobalRoleBinding", "default": {} - }, - "status": { - "default": {} } } }, @@ -5182,8 +4918,7 @@ "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -5200,9 +4935,6 @@ "spec": { "description": "Spec is the spec of the ResourcePermission", "default": {} - }, - "status": { - "default": {} } } }, @@ -5353,8 +5085,7 @@ "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -5371,9 +5102,6 @@ "spec": { "description": "Spec is the spec of the Role", "default": {} - }, - "status": { - "default": {} } } }, @@ -5381,8 +5109,7 @@ "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -5399,9 +5126,6 @@ "spec": { "description": "Spec is the spec of the RoleBinding", "default": {} - }, - "status": { - "default": {} } } }, @@ -5671,8 +5395,7 @@ "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -5689,9 +5412,6 @@ "spec": { "description": "Spec is the spec of the ServiceAccount", "default": {} - }, - "status": { - "default": {} } } }, @@ -5801,8 +5521,7 @@ "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -5819,9 +5538,6 @@ "spec": { "description": "Spec is the spec of the Team", "default": {} - }, - "status": { - "default": {} } } }, @@ -5829,8 +5545,7 @@ "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -5847,9 +5562,6 @@ "spec": { "description": "Spec is the spec of the TeamBinding", "default": {} - }, - "status": { - "default": {} } } }, @@ -6084,8 +5796,7 @@ "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -6102,9 +5813,6 @@ "spec": { "description": "Spec is the spec of the User", "default": {} - }, - "status": { - "default": {} } } }, diff --git a/pkg/tests/apis/openapi_snapshots/preferences.grafana.app-v1alpha1.json b/pkg/tests/apis/openapi_snapshots/preferences.grafana.app-v1alpha1.json index 485dafd5eea..1b9b680af34 100644 --- a/pkg/tests/apis/openapi_snapshots/preferences.grafana.app-v1alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/preferences.grafana.app-v1alpha1.json @@ -1682,8 +1682,7 @@ "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -1710,14 +1709,6 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesSpec" } ] - }, - "status": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesStatus" - } - ] } }, "x-kubernetes-group-version-kind": [ @@ -1862,66 +1853,11 @@ } } }, - "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesstatusOperatorState" - } - ] - } - } - } - }, - "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesstatusOperatorState": { - "type": "object", - "required": [ - "lastEvaluation", - "state" - ], - "properties": { - "descriptiveState": { - "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", - "type": "string" - }, - "details": { - "description": "details contains any extra information that is operator-specific", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "lastEvaluation": { - "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string", - "default": "" - }, - "state": { - "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - "type": "string", - "default": "" - } - } - }, "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars": { "type": "object", "required": [ "metadata", - "spec", - "status" + "spec" ], "properties": { "apiVersion": { @@ -1948,14 +1884,6 @@ "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsSpec" } ] - }, - "status": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsStatus" - } - ] } }, "x-kubernetes-group-version-kind": [ @@ -2055,60 +1983,6 @@ } } }, - "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsstatusOperatorState" - } - ] - } - } - } - }, - "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsstatusOperatorState": { - "type": "object", - "required": [ - "lastEvaluation", - "state" - ], - "properties": { - "descriptiveState": { - "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", - "type": "string" - }, - "details": { - "description": "details contains any extra information that is operator-specific", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "lastEvaluation": { - "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string", - "default": "" - }, - "state": { - "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", - "type": "string", - "default": "" - } - } - }, "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { "description": "APIResource specifies the name of a resource and whether it is namespaced.", "type": "object", diff --git a/public/app/api/clients/preferences/v1alpha1/endpoints.gen.ts b/public/app/api/clients/preferences/v1alpha1/endpoints.gen.ts index 2596f67306f..2ea0fd00730 100644 --- a/public/app/api/clients/preferences/v1alpha1/endpoints.gen.ts +++ b/public/app/api/clients/preferences/v1alpha1/endpoints.gen.ts @@ -678,28 +678,6 @@ export type PreferencesSpec = { /** day of the week (sunday, monday, etc) */ weekStart?: string; }; -export type PreferencesstatusOperatorState = { - /** descriptiveState is an optional more descriptive state field which has no requirements on format */ - descriptiveState?: string; - /** details contains any extra information that is operator-specific */ - details?: { - [key: string]: object; - }; - /** lastEvaluation is the ResourceVersion last evaluated */ - lastEvaluation: string; - /** state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation. */ - state: string; -}; -export type PreferencesStatus = { - /** additionalFields is reserved for future use */ - additionalFields?: { - [key: string]: object; - }; - /** operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field. */ - operatorStates?: { - [key: string]: PreferencesstatusOperatorState; - }; -}; export type Preferences = { /** 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; @@ -708,7 +686,6 @@ export type Preferences = { metadata: ObjectMeta; /** Spec is the spec of the Preferences */ spec: PreferencesSpec; - status: PreferencesStatus; }; export type ListMeta = { /** continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message. */ @@ -782,28 +759,6 @@ export type StarsResource = { export type StarsSpec = { resource: StarsResource[]; }; -export type StarsstatusOperatorState = { - /** descriptiveState is an optional more descriptive state field which has no requirements on format */ - descriptiveState?: string; - /** details contains any extra information that is operator-specific */ - details?: { - [key: string]: object; - }; - /** lastEvaluation is the ResourceVersion last evaluated */ - lastEvaluation: string; - /** state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation. */ - state: string; -}; -export type StarsStatus = { - /** additionalFields is reserved for future use */ - additionalFields?: { - [key: string]: object; - }; - /** operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field. */ - operatorStates?: { - [key: string]: StarsstatusOperatorState; - }; -}; export type Stars = { /** 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; @@ -812,7 +767,6 @@ export type Stars = { metadata: ObjectMeta; /** Spec is the spec of the Stars */ spec: StarsSpec; - status: StarsStatus; }; export type StarsList = { /** 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 */ From 5798181fb01e804ed7477a96973cef772ab919a1 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 3 Oct 2025 02:23:36 +0300 Subject: [PATCH 04/14] Provisioning: Show field errors for sync interval (#111925) --- .../features/provisioning/Wizard/FinishStep.tsx | 9 ++++++++- .../features/provisioning/utils/getFormErrors.ts | 14 +++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/public/app/features/provisioning/Wizard/FinishStep.tsx b/public/app/features/provisioning/Wizard/FinishStep.tsx index 5414c1929e2..30a4749faaf 100644 --- a/public/app/features/provisioning/Wizard/FinishStep.tsx +++ b/public/app/features/provisioning/Wizard/FinishStep.tsx @@ -12,7 +12,12 @@ import { getGitProviderFields } from './fields'; import { WizardFormData } from './types'; export const FinishStep = memo(function FinishStep() { - const { register, watch, setValue } = useFormContext(); + const { + register, + watch, + setValue, + formState: { errors }, + } = useFormContext(); const settings = useGetFrontendSettingsQuery(); const [type, readOnly] = watch(['repository.type', 'repository.readOnly']); @@ -42,6 +47,8 @@ export const FinishStep = memo(function FinishStep() { 'How often to sync changes from the repository' )} required + error={errors?.repository?.sync?.intervalSeconds?.message} + invalid={!!errors?.repository?.sync?.intervalSeconds?.message} > { 'bitbucket.url', 'git.branch', 'git.url', + 'sync.intervalSeconds', ]; + + const nestedFieldMap: Record = { + 'sync.intervalSeconds': 'repository.sync.intervalSeconds', + }; + const fieldMap: Record = { path: 'repository.path', branch: 'repository.branch', @@ -37,6 +43,12 @@ export const getFormErrors = (errors: ErrorDetails[]): FormErrorTuple => { if (error.field) { const cleanField = error.field.replace('spec.', ''); if (fieldsToValidate.includes(cleanField)) { + // Check for direct nested field mapping first + if (cleanField in nestedFieldMap) { + return [nestedFieldMap[cleanField], { message: error.detail || `Invalid ${cleanField}` }]; + } + + // Fall back to simple field mapping for non-nested fields const fieldParts = cleanField.split('.'); const lastPart = fieldParts[fieldParts.length - 1]; From 2d232aa10d7db64aaf180233bb56252937c8f524 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Thu, 2 Oct 2025 19:53:31 -0400 Subject: [PATCH 05/14] Plugins: Move store init to dskit service (#111823) --- pkg/api/plugins_test.go | 27 ++-- pkg/registry/apis/datasource/register.go | 59 ++++++-- .../backgroundsvcs/adapter/dependencies.go | 17 ++- .../backgroundsvcs/adapter/manager.go | 3 +- .../backgroundsvcs/adapter/service.go | 25 ++- .../backgroundsvcs/adapter/service_test.go | 2 +- pkg/server/wire_gen.go | 14 +- pkg/services/apiserver/service.go | 9 +- .../plugincontext/plugincontext_test.go | 4 +- .../plugininstaller/service.go | 69 +++++---- .../plugininstaller/service_test.go | 24 ++- .../pluginsintegration/pluginstore/store.go | 105 +++++++++---- .../pluginstore/store_test.go | 109 ++++++++----- .../pluginsintegration/test_helper.go | 2 +- pkg/services/provisioning/provisioning.go | 143 +++++++++--------- .../provisioning/provisioning_test.go | 4 +- 16 files changed, 394 insertions(+), 222 deletions(-) diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 4409395ebe2..cf8a2eca399 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -12,19 +12,18 @@ import ( "strings" "testing" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/auth" - "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/auth" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/filestore" @@ -528,9 +527,12 @@ func callGetPluginAsset(sc *scenarioContext) { func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, cfg *setting.Cfg, pluginRegistry registry.Service, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &fakes.FakeLoader{}, &fakes.FakeSourceRegistry{}) + require.NoError(t, err) + hs := HTTPServer{ Cfg: cfg, - pluginStore: pluginstore.New(pluginRegistry, &fakes.FakeLoader{}), + pluginStore: store, pluginFileStore: filestore.ProvideService(pluginRegistry), log: log.NewNopLogger(), pluginsCDNService: pluginscdn.ProvideService(&config.PluginManagementCfg{ @@ -640,12 +642,14 @@ func Test_PluginsList_AccessControl(t *testing.T) { for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { server := SetupAPITestServer(t, func(hs *HTTPServer) { + store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &fakes.FakeLoader{}, &fakes.FakeSourceRegistry{}) + require.NoError(t, err) + hs.Cfg = setting.NewCfg() hs.PluginSettings = &pluginSettings - hs.pluginStore = pluginstore.New(pluginRegistry, &fakes.FakeLoader{}) + hs.pluginStore = store hs.pluginFileStore = filestore.ProvideService(pluginRegistry) hs.managedPluginsService = managedplugins.NewNoop() - var err error hs.pluginsUpdateChecker, err = updatemanager.ProvidePluginsService( hs.Cfg, hs.pluginStore, @@ -828,9 +832,12 @@ func Test_PluginsSettings(t *testing.T) { for _, tc := range tcs { t.Run(tc.desc, func(t *testing.T) { server := SetupAPITestServer(t, func(hs *HTTPServer) { + store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &fakes.FakeLoader{}, &fakes.FakeSourceRegistry{}) + require.NoError(t, err) + hs.Cfg = setting.NewCfg() hs.PluginSettings = &pluginSettings - hs.pluginStore = pluginstore.New(pluginRegistry, &fakes.FakeLoader{}) + hs.pluginStore = store hs.pluginFileStore = filestore.ProvideService(pluginRegistry) errTracker := pluginerrs.ProvideErrorTracker() if tc.errCode != "" { @@ -844,7 +851,6 @@ func Test_PluginsSettings(t *testing.T) { sig := signature.ProvideService(pCfg, statickey.New()) hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, sig, hs.pluginStore) hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker) - var err error hs.pluginsUpdateChecker, err = updatemanager.ProvidePluginsService( hs.Cfg, hs.pluginStore, @@ -896,9 +902,12 @@ func Test_UpdatePluginSetting(t *testing.T) { t.Run("should return an error when trying to disable an auto-enabled plugin", func(t *testing.T) { server := SetupAPITestServer(t, func(hs *HTTPServer) { + store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &fakes.FakeLoader{}, &fakes.FakeSourceRegistry{}) + require.NoError(t, err) + hs.Cfg = setting.NewCfg() hs.PluginSettings = &pluginSettings - hs.pluginStore = pluginstore.New(pluginRegistry, &fakes.FakeLoader{}) + hs.pluginStore = store hs.pluginFileStore = filestore.ProvideService(pluginRegistry) hs.managedPluginsService = managedplugins.NewNoop() hs.log = log.NewNopLogger() diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index dc31d8cbd9e..8d24e69a03e 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -3,7 +3,9 @@ package datasource import ( "context" "encoding/json" + "errors" "fmt" + "path/filepath" "github.com/prometheus/client_golang/prometheus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,14 +22,16 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" datasource "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" + "github.com/grafana/grafana/pkg/configprovider" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/promlib/models" "github.com/grafana/grafana/pkg/registry/apis/query/queryschema" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource/kinds" ) @@ -47,12 +51,12 @@ type DataSourceAPIBuilder struct { } func RegisterAPIService( + cfgProvider configprovider.ConfigProvider, features featuremgmt.FeatureToggles, apiRegistrar builder.APIRegistrar, pluginClient plugins.Client, // access to everything datasources ScopedPluginDatasourceProvider, contextProvider PluginContextWrapper, - pluginStore pluginstore.Store, accessControl accesscontrol.AccessControl, reg prometheus.Registerer, ) (*DataSourceAPIBuilder, error) { @@ -66,25 +70,43 @@ func RegisterAPIService( var err error var builder *DataSourceAPIBuilder - all := pluginStore.Plugins(context.Background(), plugins.TypeDataSource) + + cfg, err := cfgProvider.Get(context.Background()) + if err != nil { + return nil, err + } + pluginJSONs, err := getCorePlugins(cfg) + if err != nil { + return nil, err + } + ids := []string{ "grafana-testdata-datasource", "prometheus", "graphite", } - for _, ds := range all { - if explictPluginList && !slices.Contains(ids, ds.ID) { + for _, pluginJSON := range pluginJSONs { + if explictPluginList && !slices.Contains(ids, pluginJSON.ID) { continue // skip this one } - if !ds.Backend { + if !pluginJSON.Backend { continue // skip frontend only plugins } - builder, err = NewDataSourceAPIBuilder(ds.JSONData, - pluginClient, - datasources.GetDatasourceProvider(ds.JSONData), + if pluginJSON.Type != plugins.TypeDataSource { + continue // skip non-datasource plugins + } + + client, ok := pluginClient.(PluginClient) + if !ok { + return nil, fmt.Errorf("plugin client is not a PluginClient: %T", pluginClient) + } + + builder, err = NewDataSourceAPIBuilder(pluginJSON, + client, + datasources.GetDatasourceProvider(pluginJSON), contextProvider, accessControl, features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes), @@ -277,3 +299,22 @@ func (b *DataSourceAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Op return oas, err } + +func getCorePlugins(cfg *setting.Cfg) ([]plugins.JSONData, error) { + coreDataSourcesPath := filepath.Join(cfg.StaticRootPath, "app", "plugins", "datasource") + coreDataSourcesSrc := sources.NewLocalSource( + plugins.ClassCore, + []string{coreDataSourcesPath}, + ) + + res, err := coreDataSourcesSrc.Discover(context.Background()) + if err != nil { + return nil, errors.New("failed to load core data source plugins") + } + + pluginJSONs := make([]plugins.JSONData, 0, len(res)) + for _, p := range res { + pluginJSONs = append(pluginJSONs, p.Primary.JSONData) + } + return pluginJSONs, nil +} diff --git a/pkg/registry/backgroundsvcs/adapter/dependencies.go b/pkg/registry/backgroundsvcs/adapter/dependencies.go index 20e31754490..6256ebeda8a 100644 --- a/pkg/registry/backgroundsvcs/adapter/dependencies.go +++ b/pkg/registry/backgroundsvcs/adapter/dependencies.go @@ -3,9 +3,21 @@ package adapter import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/modules" + "github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" + "github.com/grafana/grafana/pkg/services/provisioning" ) const ( + // PluginStore is the module name for the plugin store service. + PluginStore = pluginstore.ServiceName + + // PluginInstaller is the module name for the plugin installer service. + PluginInstaller = plugininstaller.ServiceName + + // Provisioning is the module name for the provisioning service. + Provisioning = provisioning.ServiceName + // Tracing is the module name for the tracing service. Tracing = tracing.ServiceName @@ -29,7 +41,10 @@ func dependencyMap() map[string][]string { return map[string][]string{ Tracing: {}, GrafanaAPIServer: {Tracing}, - Core: {GrafanaAPIServer}, + PluginStore: {GrafanaAPIServer}, + PluginInstaller: {PluginStore}, + Provisioning: {PluginStore, PluginInstaller}, + Core: {GrafanaAPIServer, PluginStore, PluginInstaller, Provisioning}, BackgroundServices: {Core}, } } diff --git a/pkg/registry/backgroundsvcs/adapter/manager.go b/pkg/registry/backgroundsvcs/adapter/manager.go index edd84418bd8..8db7d9e9c2c 100644 --- a/pkg/registry/backgroundsvcs/adapter/manager.go +++ b/pkg/registry/backgroundsvcs/adapter/manager.go @@ -14,7 +14,7 @@ import ( ) var ( - stopTimeout = 30 * time.Second + stopTimeout = 5 * time.Second ) type ManagerAdapter struct { @@ -63,6 +63,7 @@ func (m *ManagerAdapter) starting(ctx context.Context) error { // skip disabled services if s, ok := bgSvc.(registry.CanBeDisabled); ok && s.IsDisabled() { logger.Debug("Skipping disabled service", "service", namedService.ServiceName()) + manager.RegisterInvisibleModule(namedService.ServiceName(), nil) continue } diff --git a/pkg/registry/backgroundsvcs/adapter/service.go b/pkg/registry/backgroundsvcs/adapter/service.go index 037d7f392b2..50bf24b4a2d 100644 --- a/pkg/registry/backgroundsvcs/adapter/service.go +++ b/pkg/registry/backgroundsvcs/adapter/service.go @@ -18,9 +18,10 @@ var _ services.NamedService = &serviceAdapter{} // The adapter uses dskit's BasicService with a custom RunningFn: // - Starting phase: No-op, transitions immediately to Running // - Running phase: Delegates to the wrapped service's Run method -// - Stopping phase: No-op, transitions immediately to Terminated/Failed +// - Stopping phase: Closes the stop channel to signal the service to stop type serviceAdapter struct { - *services.BasicService + services.NamedService + stopCh chan struct{} name string service registry.BackgroundService } @@ -36,8 +37,9 @@ func asNamedService(service registry.BackgroundService) *serviceAdapter { a := &serviceAdapter{ name: name, service: service, + stopCh: make(chan struct{}), } - a.BasicService = services.NewBasicService(nil, a.run, nil).WithName(name) + a.NamedService = services.NewBasicService(nil, a.running, a.stopping).WithName(name) return a } @@ -46,13 +48,24 @@ func asNamedService(service registry.BackgroundService) *serviceAdapter { // background service's Run method. If the background service completes without // error, the adapter waits for context cancellation (service stop) before // transitioning to Stopping state, ensuring proper dskit service lifecycle. -func (a *serviceAdapter) run(ctx context.Context) error { - err := a.service.Run(ctx) +func (a *serviceAdapter) running(ctx context.Context) error { + serviceCtx, serviceCancel := context.WithCancel(ctx) + go func() { + <-a.stopCh + serviceCancel() + }() + + err := a.service.Run(serviceCtx) if err != nil && !errors.Is(err, context.Canceled) { return err } // wait for context cancellation to transition to Stopping state. // this prevents the service from causing it's dependents to stop prematurely. - <-ctx.Done() + <-serviceCtx.Done() + return nil +} + +func (a *serviceAdapter) stopping(_ error) error { + close(a.stopCh) return nil } diff --git a/pkg/registry/backgroundsvcs/adapter/service_test.go b/pkg/registry/backgroundsvcs/adapter/service_test.go index 368c596d395..ed357d66443 100644 --- a/pkg/registry/backgroundsvcs/adapter/service_test.go +++ b/pkg/registry/backgroundsvcs/adapter/service_test.go @@ -16,7 +16,7 @@ func TestAsNamedService(t *testing.T) { adapter := asNamedService(mockSvc) require.NotNil(t, adapter) - require.NotNil(t, adapter.BasicService) + require.NotNil(t, adapter.NamedService) require.Equal(t, mockSvc, adapter.service) expectedName := reflect.TypeOf(mockSvc).String() diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 6b99759d59a..bcbb569df18 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -547,10 +547,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } errorRegistry := pluginerrs.ProvideErrorTracker() loaderLoader := loader.ProvideService(pluginManagementCfg, discovery, bootstrap, validate, initialize, terminate, errorRegistry) - pluginstoreService, err := pluginstore.ProvideService(inMemory, sourcesService, loaderLoader) - if err != nil { - return nil, err - } + pluginstoreService := pluginstore.ProvideService(inMemory, sourcesService, loaderLoader) filestoreService := filestore.ProvideService(inMemory) fileStoreManager := dashboards.ProvideFileStoreManager(pluginstoreService, filestoreService) folderPermissionsService, err := ossaccesscontrol.ProvideFolderPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, folderimplService, acimplService, teamService, userService, actionSetService) @@ -810,7 +807,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService) snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) - dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, accessControl, registerer) + dataSourceAPIBuilder, err := datasource.RegisterAPIService(configProvider, featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer) if err != nil { return nil, err } @@ -1152,10 +1149,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } errorRegistry := pluginerrs.ProvideErrorTracker() loaderLoader := loader.ProvideService(pluginManagementCfg, discovery, bootstrap, validate, initialize, terminate, errorRegistry) - pluginstoreService, err := pluginstore.ProvideService(inMemory, sourcesService, loaderLoader) - if err != nil { - return nil, err - } + pluginstoreService := pluginstore.ProvideService(inMemory, sourcesService, loaderLoader) filestoreService := filestore.ProvideService(inMemory) fileStoreManager := dashboards.ProvideFileStoreManager(pluginstoreService, filestoreService) folderPermissionsService, err := ossaccesscontrol.ProvideFolderPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, folderimplService, acimplService, teamService, userService, actionSetService) @@ -1417,7 +1411,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService) snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) - dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, accessControl, registerer) + dataSourceAPIBuilder, err := datasource.RegisterAPIService(configProvider, featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer) if err != nil { return nil, err } diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 69381a2bc99..86706dcbb48 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -85,7 +85,6 @@ type service struct { features featuremgmt.FeatureToggles log log.Logger - stopCh chan struct{} stoppedCh chan error db db.DB @@ -148,7 +147,6 @@ func ProvideService( cfg: cfg, features: features, rr: rr, - stopCh: make(chan struct{}), builders: []builder.APIGroupBuilder{}, authorizer: authorizer.NewGrafanaBuiltInSTAuthorizer(cfg), tracing: tracing, @@ -242,11 +240,8 @@ func (s *service) Run(ctx context.Context) error { if err := s.StartAsync(ctx); err != nil { return err } - - if err := s.AwaitRunning(ctx); err != nil { - return err - } - return s.AwaitTerminated(ctx) + stopCtx := context.Background() + return s.AwaitTerminated(stopCtx) } func (s *service) RegisterAPI(b builder.APIGroupBuilder) { diff --git a/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go b/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go index feec2e53db0..eac01f6c636 100644 --- a/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go +++ b/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go @@ -41,8 +41,10 @@ func TestGet(t *testing.T) { cfg := setting.NewCfg() ds := &fakeDatasources.FakeDataSourceService{} db := &dbtest.FakeDB{ExpectedError: pluginsettings.ErrPluginSettingNotFound} + store, err := pluginstore.NewPluginStoreForTest(preg, &pluginFakes.FakeLoader{}, &pluginFakes.FakeSourceRegistry{}) + require.NoError(t, err) pcp := plugincontext.ProvideService(cfg, localcache.ProvideService(), - pluginstore.New(preg, &pluginFakes.FakeLoader{}), &fakeDatasources.FakeCacheService{}, + store, &fakeDatasources.FakeCacheService{}, ds, pluginSettings.ProvideService(db, secretstest.NewFakeSecretsService()), pluginconfig.NewFakePluginRequestConfigProvider(), ) identity := &user.SignedInUser{OrgID: int64(1), Login: "admin"} diff --git a/pkg/services/pluginsintegration/plugininstaller/service.go b/pkg/services/pluginsintegration/plugininstaller/service.go index 543d354c425..7232703820c 100644 --- a/pkg/services/pluginsintegration/plugininstaller/service.go +++ b/pkg/services/pluginsintegration/plugininstaller/service.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/grafana/dskit/services" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/repo" @@ -18,6 +19,8 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +const ServiceName = "plugin.backgroundinstaller" + var ( installRequestCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "plugins", @@ -36,6 +39,7 @@ var ( ) type Service struct { + services.NamedService cfg *setting.Cfg log log.Logger pluginInstaller plugins.Installer @@ -43,6 +47,7 @@ type Service struct { pluginRepo repo.Service features featuremgmt.FeatureToggles updateChecker pluginchecker.PluginUpdateChecker + installComplete chan struct{} // closed when all plugins are installed (used for testing) } func ProvideService( @@ -60,21 +65,18 @@ func ProvideService( }) s := &Service{ - log: log.New("plugin.backgroundinstaller"), + log: log.New(ServiceName), cfg: cfg, pluginInstaller: pluginInstaller, pluginStore: pluginStore, pluginRepo: pluginRepo, features: features, updateChecker: updateChecker, + installComplete: make(chan struct{}), } - if len(cfg.PreinstallPluginsSync) > 0 { - // Block initialization process until plugins are installed - err := s.installPluginsWithTimeout(cfg.PreinstallPluginsSync) - if err != nil { - return nil, err - } - } + + s.NamedService = services.NewBasicService(s.starting, s.running, nil).WithName(ServiceName) + return s, nil } @@ -83,24 +85,6 @@ func (s *Service) IsDisabled() bool { return len(s.cfg.PreinstallPluginsAsync) == 0 } -func (s *Service) installPluginsWithTimeout(pluginsToInstall []setting.InstallPlugin) error { - // Installation process does not timeout by default nor reuses the context - // passed to the request so we need to handle the timeout here. - // We could make this timeout configurable in the future. - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - done := make(chan struct{ err error }) - go func() { - done <- struct{ err error }{err: s.installPlugins(ctx, pluginsToInstall, true)} - }() - select { - case <-ctx.Done(): - return fmt.Errorf("failed to install plugins: %w", ctx.Err()) - case d := <-done: - return d.err - } -} - func (s *Service) shouldUpdate(ctx context.Context, pluginID, currentVersion string, pluginURL string) bool { // If the plugin is installed from a URL, we cannot check for updates as we do not have the version information // from the repository. Therefore, we assume that the plugin should be updated if the URL is provided. @@ -166,11 +150,34 @@ func (s *Service) installPlugins(ctx context.Context, pluginsToInstall []setting return nil } -func (s *Service) Run(ctx context.Context) error { - err := s.installPlugins(ctx, s.cfg.PreinstallPluginsAsync, false) - if err != nil { - // Unexpected error, asynchronous installation should not return errors - s.log.Error("Failed to install plugins", "error", err) +func (s *Service) starting(ctx context.Context) error { + if len(s.cfg.PreinstallPluginsSync) > 0 { + s.log.Info("Installing plugins", "plugins", s.cfg.PreinstallPluginsSync) + if err := s.installPlugins(ctx, s.cfg.PreinstallPluginsSync, true); err != nil { + s.log.Error("Failed to install plugins", "error", err) + return err + } } + s.log.Info("Plugins installed", "plugins", s.cfg.PreinstallPluginsSync) return nil } + +func (s *Service) running(ctx context.Context) error { + if len(s.cfg.PreinstallPluginsAsync) > 0 { + s.log.Info("Installing plugins", "plugins", s.cfg.PreinstallPluginsAsync) + if err := s.installPlugins(ctx, s.cfg.PreinstallPluginsAsync, false); err != nil { + s.log.Error("Failed to install plugins", "error", err) + return err + } + } + close(s.installComplete) + <-ctx.Done() + return nil +} + +func (s *Service) Run(ctx context.Context) error { + if err := s.StartAsync(ctx); err != nil { + return err + } + return s.AwaitTerminated(ctx) +} diff --git a/pkg/services/pluginsintegration/plugininstaller/service_test.go b/pkg/services/pluginsintegration/plugininstaller/service_test.go index 1d6c3059eac..7a846933db2 100644 --- a/pkg/services/pluginsintegration/plugininstaller/service_test.go +++ b/pkg/services/pluginsintegration/plugininstaller/service_test.go @@ -26,7 +26,7 @@ func TestService_IsDisabled(t *testing.T) { &setting.Cfg{ PreinstallPluginsAsync: []setting.InstallPlugin{{ID: "myplugin"}}, }, - pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}), + pluginstore.New(registry.NewInMemory(), &fakes.FakeLoader{}, &fakes.FakeSourceRegistry{}), &fakes.FakePluginInstaller{}, prometheus.NewRegistry(), &fakes.FakePluginRepo{}, @@ -160,12 +160,14 @@ func TestService_Run(t *testing.T) { } installed := 0 installedFromURL := 0 + store, err := pluginstore.NewPluginStoreForTest(preg, &fakes.FakeLoader{}, &fakes.FakeSourceRegistry{}) + require.NoError(t, err) s, err := ProvideService( &setting.Cfg{ PreinstallPluginsAsync: tt.pluginsToInstall, PreinstallPluginsSync: tt.pluginsToInstallSync, }, - pluginstore.New(preg, &fakes.FakeLoader{}), + store, &fakes.FakePluginInstaller{ AddFunc: func(ctx context.Context, pluginID string, version string, opts plugins.AddOpts) error { for _, plugin := range tt.pluginsToFail { @@ -203,13 +205,26 @@ func TestService_Run(t *testing.T) { &pluginchecker.FakePluginPreinstall{}, ), ) + require.NoError(t, err) + + t.Cleanup(func() { + s.StopAsync() + err := s.AwaitTerminated(context.Background()) + if tt.shouldThrowError { + require.ErrorContains(t, err, "Failed to install plugin") + return + } + require.NoError(t, err) + }) + + err = s.StartAsync(context.Background()) + require.NoError(t, err) + err = s.AwaitRunning(context.Background()) if tt.shouldThrowError { require.ErrorContains(t, err, "Failed to install plugin") return } require.NoError(t, err) - err = s.Run(context.Background()) - require.NoError(t, err) if tt.shouldInstall { expectedInstalled := 0 @@ -232,6 +247,7 @@ func TestService_Run(t *testing.T) { expectedInstalled++ } } + <-s.installComplete require.Equal(t, expectedInstalled, installed) require.Equal(t, expectedInstalledFromURL, installedFromURL) } diff --git a/pkg/services/pluginsintegration/pluginstore/store.go b/pkg/services/pluginsintegration/pluginstore/store.go index 20ef15379cf..f486218040e 100644 --- a/pkg/services/pluginsintegration/pluginstore/store.go +++ b/pkg/services/pluginsintegration/pluginstore/store.go @@ -3,18 +3,21 @@ package pluginstore import ( "context" "sort" - "sync" "time" + "github.com/grafana/dskit/services" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/sources" + "golang.org/x/sync/errgroup" ) var _ Store = (*Service)(nil) +const ServiceName = "plugins.store" + // Store is the publicly accessible storage for plugins. type Store interface { // Plugin finds a plugin by its ID. @@ -25,47 +28,81 @@ type Store interface { } type Service struct { + services.NamedService + pluginRegistry registry.Service pluginLoader loader.Service + pluginSources sources.Registry } func ProvideService(pluginRegistry registry.Service, pluginSources sources.Registry, - pluginLoader loader.Service) (*Service, error) { - ctx := context.Background() + pluginLoader loader.Service) *Service { + return New(pluginRegistry, pluginLoader, pluginSources) +} + +func (s *Service) Run(ctx context.Context) error { + if err := s.StartAsync(ctx); err != nil { + return err + } + stopCtx := context.Background() + return s.AwaitTerminated(stopCtx) +} + +func NewPluginStoreForTest(pluginRegistry registry.Service, pluginLoader loader.Service, pluginSources sources.Registry) (*Service, error) { + s := New(pluginRegistry, pluginLoader, pluginSources) + if err := s.StartAsync(context.Background()); err != nil { + return nil, err + } + if err := s.AwaitRunning(context.Background()); err != nil { + return nil, err + } + return s, nil +} + +func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginSources sources.Registry) *Service { + s := &Service{ + pluginRegistry: pluginRegistry, + pluginLoader: pluginLoader, + pluginSources: pluginSources, + } + s.NamedService = services.NewBasicService(s.starting, s.running, s.stopping).WithName(ServiceName) + return s +} + +func (s *Service) starting(ctx context.Context) error { start := time.Now() totalPlugins := 0 - logger := log.New("plugin.store") + logger := log.New(ServiceName) logger.Info("Loading plugins...") - for _, ps := range pluginSources.List(ctx) { - loadedPlugins, err := pluginLoader.Load(ctx, ps) + for _, ps := range s.pluginSources.List(ctx) { + loadedPlugins, err := s.pluginLoader.Load(ctx, ps) if err != nil { logger.Error("Loading plugin source failed", "source", ps.PluginClass(ctx), "error", err) - return nil, err + return err } - totalPlugins += len(loadedPlugins) } logger.Info("Plugins loaded", "count", totalPlugins, "duration", time.Since(start)) - return New(pluginRegistry, pluginLoader), nil + return nil } -func (s *Service) Run(ctx context.Context) error { +func (s *Service) running(ctx context.Context) error { <-ctx.Done() - s.shutdown(ctx) - return ctx.Err() + return nil } -func New(pluginRegistry registry.Service, pluginLoader loader.Service) *Service { - return &Service{ - pluginRegistry: pluginRegistry, - pluginLoader: pluginLoader, - } +func (s *Service) stopping(failureReason error) error { + return s.shutdown(context.Background()) } func (s *Service) Plugin(ctx context.Context, pluginID string) (Plugin, bool) { + if err := s.AwaitRunning(ctx); err != nil { + log.New(ServiceName).FromContext(ctx).Error("Failed to get plugin", "error", err) + return Plugin{}, false + } p, exists := s.plugin(ctx, pluginID) if !exists { return Plugin{}, false @@ -75,6 +112,10 @@ func (s *Service) Plugin(ctx context.Context, pluginID string) (Plugin, bool) { } func (s *Service) Plugins(ctx context.Context, pluginTypes ...plugins.Type) []Plugin { + if err := s.AwaitRunning(ctx); err != nil { + log.New(ServiceName).FromContext(ctx).Error("Failed to get plugins", "error", err) + return []Plugin{} + } // if no types passed, assume all if len(pluginTypes) == 0 { pluginTypes = plugins.PluginTypes @@ -125,6 +166,10 @@ func (s *Service) availablePlugins(ctx context.Context) []*plugins.Plugin { } func (s *Service) Routes(ctx context.Context) []*plugins.StaticRoute { + if err := s.AwaitRunning(ctx); err != nil { + log.New(ServiceName).FromContext(ctx).Error("Failed to get routes", "error", err) + return []*plugins.StaticRoute{} + } staticRoutes := make([]*plugins.StaticRoute, 0) for _, p := range s.availablePlugins(ctx) { @@ -135,18 +180,20 @@ func (s *Service) Routes(ctx context.Context) []*plugins.StaticRoute { return staticRoutes } -func (s *Service) shutdown(ctx context.Context) { - var wg sync.WaitGroup - for _, plugin := range s.pluginRegistry.Plugins(ctx) { - wg.Add(1) - go func(ctx context.Context, p *plugins.Plugin) { - defer wg.Done() - p.Logger().Debug("Stopping plugin") - if _, err := s.pluginLoader.Unload(ctx, p); err != nil { - p.Logger().Error("Failed to stop plugin", "error", err) +func (s *Service) shutdown(ctx context.Context) error { + var errgroup errgroup.Group + plugins := s.pluginRegistry.Plugins(ctx) + for _, p := range plugins { + plugin := p // capture loop variable + errgroup.Go(func() error { + plugin.Logger().Debug("Stopping plugin") + if _, err := s.pluginLoader.Unload(ctx, plugin); err != nil { + plugin.Logger().Error("Failed to stop plugin", "error", err) + return err } - p.Logger().Debug("Plugin stopped") - }(ctx, plugin) + plugin.Logger().Debug("Plugin stopped") + return nil + }) } - wg.Wait() + return errgroup.Wait() } diff --git a/pkg/services/pluginsintegration/pluginstore/store_test.go b/pkg/services/pluginsintegration/pluginstore/store_test.go index b0c0b408cf9..e195fe5e932 100644 --- a/pkg/services/pluginsintegration/pluginstore/store_test.go +++ b/pkg/services/pluginsintegration/pluginstore/store_test.go @@ -2,7 +2,7 @@ package pluginstore import ( "context" - "sync" + "errors" "testing" "github.com/stretchr/testify/require" @@ -43,7 +43,11 @@ func TestStore_ProvideService(t *testing.T) { } }} - _, err := ProvideService(fakes.NewFakePluginRegistry(), srcs, l) + service := ProvideService(fakes.NewFakePluginRegistry(), srcs, l) + ctx := context.Background() + err := service.StartAsync(ctx) + require.NoError(t, err) + err = service.AwaitRunning(ctx) require.NoError(t, err) require.Equal(t, []plugins.Class{"1", "2", "3"}, loadedSrcs) }) @@ -55,12 +59,13 @@ func TestStore_Plugin(t *testing.T) { p1.RegisterClient(&DecommissionedPlugin{}) p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-panel"}} - ps := New(&fakes.FakePluginRegistry{ + ps, err := NewPluginStoreForTest(&fakes.FakePluginRegistry{ Store: map[string]*plugins.Plugin{ p1.ID: p1, p2.ID: p2, }, - }, &fakes.FakeLoader{}) + }, &fakes.FakeLoader{}, &fakes.FakeSourceRegistry{}) + require.NoError(t, err) p, exists := ps.Plugin(context.Background(), p1.ID) require.False(t, exists) @@ -81,7 +86,7 @@ func TestStore_Plugins(t *testing.T) { p5 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "e-test-panel", Type: plugins.TypePanel}} p5.RegisterClient(&DecommissionedPlugin{}) - ps := New(&fakes.FakePluginRegistry{ + ps, err := NewPluginStoreForTest(&fakes.FakePluginRegistry{ Store: map[string]*plugins.Plugin{ p1.ID: p1, p2.ID: p2, @@ -89,7 +94,8 @@ func TestStore_Plugins(t *testing.T) { p4.ID: p4, p5.ID: p5, }, - }, &fakes.FakeLoader{}) + }, &fakes.FakeLoader{}, &fakes.FakeSourceRegistry{}) + require.NoError(t, err) ToGrafanaDTO(p1) pss := ps.Plugins(context.Background()) @@ -124,7 +130,7 @@ func TestStore_Routes(t *testing.T) { p6 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "f-test-app", Type: plugins.TypeApp}} p6.RegisterClient(&DecommissionedPlugin{}) - ps := New(&fakes.FakePluginRegistry{ + ps, err := NewPluginStoreForTest(&fakes.FakePluginRegistry{ Store: map[string]*plugins.Plugin{ p1.ID: p1, p2.ID: p2, @@ -132,7 +138,8 @@ func TestStore_Routes(t *testing.T) { p5.ID: p5, p6.ID: p6, }, - }, &fakes.FakeLoader{}) + }, &fakes.FakeLoader{}, &fakes.FakeSourceRegistry{}) + require.NoError(t, err) sr := func(p *plugins.Plugin) *plugins.StaticRoute { return &plugins.StaticRoute{PluginID: p.ID, Directory: p.FS.Base()} @@ -144,39 +151,62 @@ func TestStore_Routes(t *testing.T) { } func TestProcessManager_shutdown(t *testing.T) { - p := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource", Type: plugins.TypeDataSource}} // Backend: true - backend := &fakes.FakeBackendPlugin{} - p.RegisterClient(backend) - p.SetLogger(log.NewTestLogger()) + t.Run("When context is cancelled the plugin is stopped", func(t *testing.T) { + p := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource", Type: plugins.TypeDataSource}} // Backend: true + backend := &fakes.FakeBackendPlugin{} + p.RegisterClient(backend) + p.SetLogger(log.NewTestLogger()) - unloaded := false - ps := New(&fakes.FakePluginRegistry{ - Store: map[string]*plugins.Plugin{ - p.ID: p, - }, - }, &fakes.FakeLoader{ - UnloadFunc: func(_ context.Context, plugin *plugins.Plugin) (*plugins.Plugin, error) { - require.Equal(t, p, plugin) - unloaded = true - return nil, nil - }, + unloaded := false + ps := New(&fakes.FakePluginRegistry{ + Store: map[string]*plugins.Plugin{ + p.ID: p, + }, + }, &fakes.FakeLoader{ + UnloadFunc: func(_ context.Context, plugin *plugins.Plugin) (*plugins.Plugin, error) { + require.Equal(t, p, plugin) + unloaded = true + return nil, nil + }, + }, &fakes.FakeSourceRegistry{}) + + ctx, cancel := context.WithCancel(context.Background()) + + err := ps.StartAsync(ctx) + require.NoError(t, err) + err = ps.AwaitRunning(ctx) + require.NoError(t, err) + + // Cancel context to trigger shutdown + cancel() + + // Wait for service to be fully terminated + err = ps.AwaitTerminated(context.Background()) + require.NoError(t, err) + require.True(t, unloaded) }) - pCtx := context.Background() - cCtx, cancel := context.WithCancel(pCtx) - var wgRun sync.WaitGroup - wgRun.Add(1) - var runErr error - go func() { - runErr = ps.Run(cCtx) - wgRun.Done() - }() + t.Run("When shutdown fails, stopping method returns error", func(t *testing.T) { + p := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-datasource", Type: plugins.TypeDataSource}} + backend := &fakes.FakeBackendPlugin{} + p.RegisterClient(backend) + p.SetLogger(log.NewTestLogger()) - t.Run("When context is cancelled the plugin is stopped", func(t *testing.T) { - cancel() - wgRun.Wait() - require.ErrorIs(t, runErr, context.Canceled) - require.True(t, unloaded) + expectedErr := errors.New("unload failed") + ps, err := NewPluginStoreForTest(&fakes.FakePluginRegistry{ + Store: map[string]*plugins.Plugin{ + p.ID: p, + }, + }, &fakes.FakeLoader{ + UnloadFunc: func(_ context.Context, plugin *plugins.Plugin) (*plugins.Plugin, error) { + return nil, expectedErr + }, + }, &fakes.FakeSourceRegistry{}) + require.NoError(t, err) + + err = ps.stopping(nil) + require.Error(t, err) + require.ErrorIs(t, err, expectedErr) }) } @@ -186,12 +216,13 @@ func TestStore_availablePlugins(t *testing.T) { p1.RegisterClient(&DecommissionedPlugin{}) p2 := &plugins.Plugin{JSONData: plugins.JSONData{ID: "test-app"}} - ps := New(&fakes.FakePluginRegistry{ + ps, err := NewPluginStoreForTest(&fakes.FakePluginRegistry{ Store: map[string]*plugins.Plugin{ p1.ID: p1, p2.ID: p2, }, - }, &fakes.FakeLoader{}) + }, &fakes.FakeLoader{}, &fakes.FakeSourceRegistry{}) + require.NoError(t, err) aps := ps.availablePlugins(context.Background()) require.Len(t, aps, 1) diff --git a/pkg/services/pluginsintegration/test_helper.go b/pkg/services/pluginsintegration/test_helper.go index 8fec1925eb3..6689430c43b 100644 --- a/pkg/services/pluginsintegration/test_helper.go +++ b/pkg/services/pluginsintegration/test_helper.go @@ -67,7 +67,7 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core Terminator: term, }) - ps, err := pluginstore.ProvideService(reg, sources.ProvideService(cfg, pCfg), l) + ps, err := pluginstore.NewPluginStoreForTest(reg, l, sources.ProvideService(cfg, pCfg)) require.NoError(t, err) return &IntegrationTestCtx{ diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index b40b6136a46..e7e8c3c5fa7 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -7,6 +7,7 @@ import ( "path/filepath" "sync" + "github.com/grafana/dskit/services" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -39,6 +40,8 @@ import ( "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" ) +const ServiceName = "provisioning" + func ProvideService( ac accesscontrol.AccessControl, cfg *setting.Cfg, @@ -91,6 +94,8 @@ func ProvideService( dual: dual, } + s.NamedService = services.NewBasicService(s.starting, s.running, nil).WithName(ServiceName) + if err := s.setDashboardProvisioner(); err != nil { return nil, err } @@ -98,6 +103,67 @@ func ProvideService( return s, nil } +func (ps *ProvisioningServiceImpl) starting(ctx context.Context) error { + if err := ps.ProvisionDatasources(ctx); err != nil { + ps.log.Error("Failed to provision data sources", "error", err) + return err + } + + if err := ps.ProvisionPlugins(ctx); err != nil { + ps.log.Error("Failed to provision plugins", "error", err) + return err + } + + if err := ps.ProvisionAlerting(ctx); err != nil { + ps.log.Error("Failed to provision alerting", "error", err) + return err + } + + // Migrating prom types relies on data source provisioning to already be completed + // If we can make services depend on other services completing first, + // then we should remove this from provisioning + if err := ps.migratePrometheusType(ctx); err != nil { + ps.log.Error("Failed to migrate Prometheus type", "error", err) + return err + } + + if err := ps.ProvisionDashboards(ctx); err != nil { + ps.log.Error("Failed to provision dashboard", "error", err) + // Consider the allow list of errors for which running the provisioning service should not + // fail. For now this includes only dashboards.ErrGetOrCreateFolder. + if !errors.Is(err, dashboards.ErrGetOrCreateFolder) { + return err + } + } + if ps.dashboardProvisioner.HasDashboardSources() { + ps.searchService.TriggerReIndex() + } + return nil +} + +func (ps *ProvisioningServiceImpl) running(ctx context.Context) error { + for { + // Wait for unlock. This is tied to new dashboardProvisioner to be instantiated before we start polling. + ps.mutex.Lock() + // Using background here because otherwise if root context was canceled the select later on would + // non-deterministically take one of the route possibly going into one polling loop before exiting. + pollingContext, cancelFun := context.WithCancel(context.Background()) + ps.pollingCtxCancel = cancelFun + ps.dashboardProvisioner.PollChanges(pollingContext) + ps.mutex.Unlock() + + select { + case <-pollingContext.Done(): + // Polling was canceled. + continue + case <-ctx.Done(): + // Root server context was cancelled so cancel polling and leave. + ps.cancelPolling() + return nil + } + } +} + func (ps *ProvisioningServiceImpl) setDashboardProvisioner() error { dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards") dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.orgService, ps.dashboardService, ps.folderService, ps.dual) @@ -137,6 +203,8 @@ func newProvisioningServiceImpl( migratePrometheusType: migratePrometheusType, } + s.NamedService = services.NewBasicService(s.starting, s.running, nil).WithName(ServiceName) + if err := s.setDashboardProvisioner(); err != nil { return nil, err } @@ -145,6 +213,7 @@ func newProvisioningServiceImpl( } type ProvisioningServiceImpl struct { + services.NamedService Cfg *setting.Cfg SQLStore db.DB orgService org.Service @@ -173,7 +242,6 @@ type ProvisioningServiceImpl struct { resourcePermissions accesscontrol.ReceiverPermissionsService tracer tracing.Tracer dual dualwrite.Service - onceInitProvisioners sync.Once migratePrometheusType func(context.Context) error } @@ -185,78 +253,11 @@ func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) erro } func (ps *ProvisioningServiceImpl) Run(ctx context.Context) error { - var err error - - // Run Datasources, Plugins and Alerting Provisioning only once. - // It can't be initialized at RunInitProvisioners because it - // depends on the /apis endpoints to be already running and listeningq - ps.onceInitProvisioners.Do(func() { - err = ps.ProvisionDatasources(ctx) - if err != nil { - ps.log.Error("Failed to provision data sources", "error", err) - return - } - - err = ps.ProvisionPlugins(ctx) - if err != nil { - ps.log.Error("Failed to provision plugins", "error", err) - return - } - - err = ps.ProvisionAlerting(ctx) - if err != nil { - ps.log.Error("Failed to provision alerting", "error", err) - return - } - - // Migrating prom types relies on data source provisioning to already be completed - // If we can make services depend on other services completing first, - // then we should remove this from provisioning - err = ps.migratePrometheusType(ctx) - if err != nil { - ps.log.Error("Failed to migrate Prometheus type", "error", err) - return - } - }) - - if err != nil { - // error already logged + if err := ps.StartAsync(ctx); err != nil { return err } - - err = ps.ProvisionDashboards(ctx) - if err != nil { - ps.log.Error("Failed to provision dashboard", "error", err) - // Consider the allow list of errors for which running the provisioning service should not - // fail. For now this includes only dashboards.ErrGetOrCreateFolder. - if !errors.Is(err, dashboards.ErrGetOrCreateFolder) { - return err - } - } - if ps.dashboardProvisioner.HasDashboardSources() { - ps.searchService.TriggerReIndex() - } - - for { - // Wait for unlock. This is tied to new dashboardProvisioner to be instantiated before we start polling. - ps.mutex.Lock() - // Using background here because otherwise if root context was canceled the select later on would - // non-deterministically take one of the route possibly going into one polling loop before exiting. - pollingContext, cancelFun := context.WithCancel(context.Background()) - ps.pollingCtxCancel = cancelFun - ps.dashboardProvisioner.PollChanges(pollingContext) - ps.mutex.Unlock() - - select { - case <-pollingContext.Done(): - // Polling was canceled. - continue - case <-ctx.Done(): - // Root server context was cancelled so cancel polling and leave. - ps.cancelPolling() - return ctx.Err() - } - } + stopCtx := context.Background() + return ps.AwaitTerminated(stopCtx) } func (ps *ProvisioningServiceImpl) ProvisionDatasources(ctx context.Context) error { diff --git a/pkg/services/provisioning/provisioning_test.go b/pkg/services/provisioning/provisioning_test.go index ee4abc5029b..8b513ba321e 100644 --- a/pkg/services/provisioning/provisioning_test.go +++ b/pkg/services/provisioning/provisioning_test.go @@ -48,7 +48,7 @@ func TestProvisioningServiceImpl(t *testing.T) { serviceTest.waitForStop() assert.False(t, serviceTest.serviceRunning, "Service should not be running") - assert.Equal(t, context.Canceled, serviceTest.serviceError, "Service should have returned canceled error") + assert.NoError(t, serviceTest.serviceError, "Service should not have returned an error") }) t.Run("Failed reloading does not stop polling with old provisioned", func(t *testing.T) { @@ -91,7 +91,7 @@ func TestProvisioningServiceImpl(t *testing.T) { serviceTest.cancel() serviceTest.waitForStop() - assert.Equal(t, context.Canceled, serviceTest.serviceError) + assert.NoError(t, serviceTest.serviceError, "Service should not have returned an error") }) t.Run("Should return run error when dashboard provisioning fails for non-allow-listed error", func(t *testing.T) { From 2f321982b291f9be5d47127f2f34c8e8e6e4fbd2 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 00:37:30 +0000 Subject: [PATCH 06/14] I18n: Download translations from Crowdin (#111970) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 1 + public/locales/de-DE/grafana.json | 1 + public/locales/es-ES/grafana.json | 1 + public/locales/fr-FR/grafana.json | 1 + public/locales/hu-HU/grafana.json | 1 + public/locales/id-ID/grafana.json | 1 + public/locales/it-IT/grafana.json | 1 + public/locales/ja-JP/grafana.json | 1 + public/locales/ko-KR/grafana.json | 1 + public/locales/nl-NL/grafana.json | 1 + public/locales/pl-PL/grafana.json | 1 + public/locales/pt-BR/grafana.json | 1 + public/locales/pt-PT/grafana.json | 1 + public/locales/ru-RU/grafana.json | 1 + public/locales/sv-SE/grafana.json | 1 + public/locales/tr-TR/grafana.json | 1 + public/locales/zh-Hans/grafana.json | 1 + public/locales/zh-Hant/grafana.json | 1 + 18 files changed, 18 insertions(+) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 7a4a9c5fb99..2f3a92ee6ea 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -3520,6 +3520,7 @@ "move-modal-field-label": "Název složky", "move-modal-text": "Tato akce přesune následující obsah:", "move-modal-title": "Přesunout", + "move-provisioned-folder": "", "moving": "Probíhá přesouvání…", "new-folder-name-required-phrase": "Název složky je povinný.", "selected-mix-resources-modal-text": "Vybrali jste přidělené i nepřidělené zdroje. Tyto zdroje nelze zpracovat společně. Vyberte pouze přidělené nebo nepřidělené zdroje a zkuste to znovu.", diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index bb98fc3edcc..eb8f0d1d395 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -3496,6 +3496,7 @@ "move-modal-field-label": "Ordnername", "move-modal-text": "Diese Aktion wird folgenden Inhalt verschieben:", "move-modal-title": "Verschieben", + "move-provisioned-folder": "", "moving": "Verschieben...", "new-folder-name-required-phrase": "Ordnername ist erforderlich.", "selected-mix-resources-modal-text": "Sie haben sowohl bereitgestellte als auch nicht bereitgestellte Ressourcen ausgewählt. Diese können nicht zusammen verarbeitet werden. Bitte wählen Sie nur bereitgestellte Ressourcen oder nur nicht bereitgestellte Ressourcen aus und versuchen Sie es dann erneut.", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 763065d33c1..099476d81f0 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -3496,6 +3496,7 @@ "move-modal-field-label": "Nombre de la carpeta", "move-modal-text": "Esta acción moverá el siguiente contenido:", "move-modal-title": "Mover", + "move-provisioned-folder": "", "moving": "Moviendo...", "new-folder-name-required-phrase": "Se requiere el nombre de la carpeta.", "selected-mix-resources-modal-text": "Has seleccionado recursos aprovisionados y no aprovisionados. No se pueden procesar juntos. Selecciona solo recursos aprovisionados o solo recursos no aprovisionados e inténtalo de nuevo.", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index c99717bb9c5..81e72c3a19b 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -3496,6 +3496,7 @@ "move-modal-field-label": "Nom du dossier", "move-modal-text": "Cette action déplacera le contenu suivant :", "move-modal-title": "Déplacer", + "move-provisioned-folder": "", "moving": "Déplacement...", "new-folder-name-required-phrase": "Vous devez saisir le nom du dossier.", "selected-mix-resources-modal-text": "Vous avez sélectionné à la fois des ressources provisionnées et non provisionnées. Ces deux types ne peuvent être traités simultanément. Veuillez sélectionner uniquement des ressources provisionnées ou uniquement des ressources non provisionnées, puis réessayer.", diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 258bdc1e86e..d3413be6f75 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -3496,6 +3496,7 @@ "move-modal-field-label": "Mappa neve", "move-modal-text": "Ez a művelet áthelyezi a következő tartalmat:", "move-modal-title": "Áthelyezés", + "move-provisioned-folder": "", "moving": "Áthelyezés...", "new-folder-name-required-phrase": "A mappa nevének megadása kötelező.", "selected-mix-resources-modal-text": "Kiépített és nem kiépített erőforrásokat is kijelölt. Ezeket nem lehet egyszerre feldolgozni. Kérjük, válasszon csak kiépített vagy csak nem kiépített erőforrásokat, majd próbálja újra.", diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 73455bde660..4e1f1914d6c 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -3484,6 +3484,7 @@ "move-modal-field-label": "Nama folder", "move-modal-text": "Tindakan ini akan memindahkan konten berikut:", "move-modal-title": "Pindahkan", + "move-provisioned-folder": "", "moving": "Memindahkan...", "new-folder-name-required-phrase": "Nama folder wajib diisi.", "selected-mix-resources-modal-text": "Anda telah memilih sumber daya yang disediakan dan tidak disediakan. Keduanya tidak dapat diproses sekaligus. Hanya pilih sumber daya yang disediakan atau hanya sumber daya yang tidak disediakan dan coba lagi.", diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index d70def019ff..749e12ad04e 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -3496,6 +3496,7 @@ "move-modal-field-label": "Nome cartella", "move-modal-text": "Questa azione sposterà il seguente contenuto:", "move-modal-title": "Sposta", + "move-provisioned-folder": "", "moving": "Spostamento in corso...", "new-folder-name-required-phrase": "Il nome della cartella è obbligatorio.", "selected-mix-resources-modal-text": "Hai selezionato sia risorse sottoposte a provisioning che risorse non sottoposte a provisioning. Non possono essere elaborate insieme. Seleziona solo risorse sottoposte a provisioning o solo risorse non sottoposte a provisioning e riprova.", diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 4d639e10f2d..59de942e78e 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -3484,6 +3484,7 @@ "move-modal-field-label": "フォルダ名", "move-modal-text": "この操作により、次のコンテンツが移動されます。", "move-modal-title": "移動", + "move-provisioned-folder": "", "moving": "移動中…", "new-folder-name-required-phrase": "フォルダ名が必要です。", "selected-mix-resources-modal-text": "プロビジョニング済みのリソースと未プロビジョニングのリソースの両方が選択されています。これらは同時に処理できません。プロビジョニングされたリソースのみまたはプロビジョニングされていないリソースのみを選択して、再試行してください。", diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 990f9805176..c93e2bb92a4 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -3484,6 +3484,7 @@ "move-modal-field-label": "폴더 이름", "move-modal-text": "이 작업을 수행하면 다음 콘텐츠가 이동됩니다.", "move-modal-title": "이동", + "move-provisioned-folder": "", "moving": "이동 중...", "new-folder-name-required-phrase": "폴더 이름은 필수 입력 항목입니다.", "selected-mix-resources-modal-text": "프로비저닝된 리소스와 프로비저닝되지 않은 리소스를 모두 선택하셨습니다. 이 두 가지는 함께 처리할 수 없습니다. 프로비저닝된 리소스만 또는 프로비저닝되지 않은 리소스만 선택한 후 다시 시도해 주세요.", diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 259e3cccb50..deb2897c99c 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -3496,6 +3496,7 @@ "move-modal-field-label": "Mapnaam", "move-modal-text": "Met deze actie wordt de volgende inhoud verplaatst:", "move-modal-title": "Verplaatsen", + "move-provisioned-folder": "", "moving": "Bezig met verplaatsen ...", "new-folder-name-required-phrase": "Mapnaam is vereist.", "selected-mix-resources-modal-text": "Je hebt zowel toegewezen als niet-toegewezen bronnen geselecteerd. Deze kunnen niet samen worden verwerkt. Selecteer alleen toegewezen bronnen of alleen niet-toegewezen bronnen en probeer het opnieuw.", diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index ad752de6f02..3aa46a8c694 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -3520,6 +3520,7 @@ "move-modal-field-label": "Nazwa folderu", "move-modal-text": "To działanie spowoduje przeniesienie następującej zawartości:", "move-modal-title": "Przenieś", + "move-provisioned-folder": "", "moving": "Przenoszenie…", "new-folder-name-required-phrase": "Wymagana jest nazwa folderu.", "selected-mix-resources-modal-text": "Wybrano zarówno zasoby udostępnione, jak i nieudostępnione. Nie można ich przetwarzać razem. Wybierz tylko zasoby udostępnione lub tylko zasoby nieudostępnione i spróbuj ponownie.", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 9af03ad58fb..7c0d99cec65 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -3496,6 +3496,7 @@ "move-modal-field-label": "Nome da pasta", "move-modal-text": "Esta ação moverá o seguinte conteúdo:", "move-modal-title": "Mover", + "move-provisioned-folder": "", "moving": "Movendo...", "new-folder-name-required-phrase": "O nome da pasta é obrigatório.", "selected-mix-resources-modal-text": "Você selecionou recursos provisionados e não provisionados. Não éo possível processá-los juntos. Selecione apenas um dos tipos de recurso e tente novamente.", diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 7d58847a6f5..6e87ec75ca5 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -3496,6 +3496,7 @@ "move-modal-field-label": "Nome da pasta", "move-modal-text": "Esta ação moverá o seguinte conteúdo:", "move-modal-title": "Mover", + "move-provisioned-folder": "", "moving": "A mover...", "new-folder-name-required-phrase": "O nome da pasta é obrigatório.", "selected-mix-resources-modal-text": "Selecionou recursos provisionados e não provisionados. Estes não podem ser processados em conjunto. Selecione apenas recursos provisionados ou apenas recursos não provisionados e tente novamente.", diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index c6dbc108ff9..f95e047aa37 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -3520,6 +3520,7 @@ "move-modal-field-label": "Имя папки", "move-modal-text": "Это действие приведет к перемещению следующего контента:", "move-modal-title": "Перемещение", + "move-provisioned-folder": "", "moving": "Перемещение...", "new-folder-name-required-phrase": "Необходимо указать имя папки.", "selected-mix-resources-modal-text": "Вы выбрали как подготовленные, так и неподготовленные ресурсы. Совместная обработка невозможна. Выберите только подготовленные или неподготовленные ресурсы и повторите попытку.", diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 840a0d68dc4..e9e31498880 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -3496,6 +3496,7 @@ "move-modal-field-label": "Mappnamn", "move-modal-text": "Denna åtgärd flyttar följande innehåll:", "move-modal-title": "Flytta", + "move-provisioned-folder": "", "moving": "Flyttar ...", "new-folder-name-required-phrase": "Mappnamn krävs.", "selected-mix-resources-modal-text": "Du har valt både provisionerade och icke-provisionerade resurser. Dessa kan inte hanteras tillsammans. Välj enbart provisionerade eller enbart icke-provisionerade resurser och försök igen.", diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 884dc555c5f..1e97fd70be4 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -3496,6 +3496,7 @@ "move-modal-field-label": "Klasör adı", "move-modal-text": "Bu işlem aşağıdaki içerikleri taşıyacaktır:", "move-modal-title": "Taşı", + "move-provisioned-folder": "", "moving": "Taşınıyor...", "new-folder-name-required-phrase": "Klasör adı gereklidir.", "selected-mix-resources-modal-text": "Hem sağlanmış hem de sağlanmamış kaynakları seçtiniz. Bunlar birlikte işlenemez. Lütfen yalnızca sağlanmış kaynakları veya yalnızca sağlanmamış kaynakları seçin ve tekrar deneyin.", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 68d83a91518..e03b3dffc2a 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -3484,6 +3484,7 @@ "move-modal-field-label": "文件夹名称", "move-modal-text": "此操作将移动以下内容:", "move-modal-title": "移动", + "move-provisioned-folder": "", "moving": "正在移动...", "new-folder-name-required-phrase": "文件夹名称是必需项。", "selected-mix-resources-modal-text": "您已选择了已预置和未预置的资源。这些资源无法一起处理。请仅选择已预置的资源或未预置的资源,然后重试。", diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index fc24819d7ec..1152d08508e 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -3484,6 +3484,7 @@ "move-modal-field-label": "資料夾名稱", "move-modal-text": "此動作將移動以下內容:", "move-modal-title": "移動", + "move-provisioned-folder": "", "moving": "正在移動…", "new-folder-name-required-phrase": "必須要有資料夾名稱。", "selected-mix-resources-modal-text": "您已選取已佈建和非佈建資源。這些資源無法一起處理。請僅選取已佈建的資源或僅選取非佈建資源,然後再試一次。", From 454cd0f38585d3e93b31daf06a25e808fb6f9b17 Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Thu, 2 Oct 2025 22:51:45 -0400 Subject: [PATCH 07/14] BrowseView: Git Provisioned Read Only Folder Disable Empty State Create Dashboard Button (#111971) BrowseView: Disable empty state create dashboard button when repo is read only --- .../features/browse-dashboards/BrowseDashboardsPage.tsx | 8 +++++++- .../features/browse-dashboards/components/BrowseView.tsx | 6 ++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index c658fbaa6cd..bc9572efdbd 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -206,7 +206,13 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record ) : ( - + ) } diff --git a/public/app/features/browse-dashboards/components/BrowseView.tsx b/public/app/features/browse-dashboards/components/BrowseView.tsx index 1c98b2d440a..19b6ee383fc 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.tsx @@ -33,9 +33,10 @@ interface BrowseViewProps { width: number; folderUID: string | undefined; permissions: BrowseDashboardsPermissions; + isReadOnlyRepo?: boolean; } -export function BrowseView({ folderUID, width, height, permissions }: BrowseViewProps) { +export function BrowseView({ folderUID, width, height, permissions, isReadOnlyRepo }: BrowseViewProps) { const status = useBrowseLoadingStatus(folderUID); const dispatch = useDispatch(); const flatTree = useFlatTreeState(folderUID); @@ -163,6 +164,7 @@ export function BrowseView({ folderUID, width, height, permissions }: BrowseView href={folderUID ? `dashboard/new?folderUid=${folderUID}` : 'dashboard/new'} icon="plus" size="lg" + disabled={isReadOnlyRepo} > Create dashboard @@ -173,7 +175,7 @@ export function BrowseView({ folderUID, width, height, permissions }: BrowseView : t('browse-dashboards.empty-state.title', "You haven't created any dashboards yet") } > - {folderUID && ( + {folderUID && !isReadOnlyRepo && ( Add/move dashboards to your folder at{' '} From 7ed46fd32138e98c617cab8e949262a3867fc0cf Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:02:58 +0200 Subject: [PATCH 08/14] docs(alerting): `alertingSaveStateCompressed` is enabled by default (#111897) --- .../set-up/performance-limitations/index.md | 16 ++++++++-------- .../configure-grafana/feature-toggles/index.md | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/sources/alerting/set-up/performance-limitations/index.md b/docs/sources/alerting/set-up/performance-limitations/index.md index 592504d9a91..82e8ccd41f1 100644 --- a/docs/sources/alerting/set-up/performance-limitations/index.md +++ b/docs/sources/alerting/set-up/performance-limitations/index.md @@ -58,20 +58,20 @@ For more information, refer to [this GitHub issue](https://github.com/grafana/gr ## High load on database caused by a high number of alert instances -If you have a high number of alert instances, it can happen that the load on the database gets very high, as each state -transition of an alert instance is saved in the database after every evaluation. +If you have a high number of alert rules or alert instances, the load on the database can get very high. -### Compressed alert state +By default, Grafana performs one SQL update per alert rule after each evaluation, which updates all alert instances belonging to the rule. -When the `alertingSaveStateCompressed` feature toggle is enabled, Grafana saves the alert rule state in a compressed form. Instead of performing an individual SQL update for each alert instance, Grafana performs a single SQL update per alert rule, updating all alert instances belonging to that rule. - -This can significantly reduce database overhead for alert rules with many alert instances. +You can change this behavior by disabling the `alertingSaveStateCompressed` feature flag. In this case, Grafana performs a separate SQL update for each state change of an alert instance. This configuration is rarely recommended, as it can add significant database overhead for alert rules with many instances. ### Save state periodically -High load can be also prevented by writing to the database periodically, instead of after every evaluation. +You can also reduce database load by writing states periodically instead of after every evaluation. -To save state periodically, enable the `alertingSaveStatePeriodic` feature toggle. +To save state periodically: + +1. Enable the `alertingSaveStatePeriodic` feature toggle. +1. Disable the `alertingSaveStateCompressed` feature toggle. By default, it saves the states every 5 minutes to the database and on each shutdown. The periodic interval can also be configured using the `state_periodic_save_interval` configuration flag. During this process, Grafana deletes all existing alert instances from the database and then writes the entire current set of instances back in batches in a single transaction. diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index f387da5857b..e8b12113400 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -101,7 +101,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `pdfTables` | Enables generating table data as PDF in reporting | | `canvasPanelPanZoom` | Allow pan and zoom in canvas panel | | `regressionTransformation` | Enables regression analysis transformation | -| `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage | +| `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage. Default is enabled. | | `sqlExpressions` | Enables SQL Expressions, which can execute SQL queries against data source results. | | `queryLibrary` | Enables Saved queries (query library) feature | | `enableSCIM` | Enables SCIM support for user and group management | From add8beefad4eb330c2a6ce05710510bc8ad7428e Mon Sep 17 00:00:00 2001 From: Gareth Date: Fri, 3 Oct 2025 15:06:25 +0800 Subject: [PATCH 09/14] Logs: Fix spacing between the logs volume and logs panel (#111952) Logs: fix spacing between volume and logs panel --- public/app/features/explore/Explore.tsx | 2 +- public/app/features/explore/Logs/Logs.tsx | 49 +++++++++++++---------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index f9660d5833a..07a02cc17ae 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -77,7 +77,7 @@ const getStyles = (theme: GrafanaTheme2) => { label: 'exploreMain', // Is needed for some transition animations to work. position: 'relative', - marginTop: '21px', + marginTop: theme.spacing(3), display: 'flex', flexDirection: 'column', gap: theme.spacing(1), diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index fd1d23675c5..3c505a5aeea 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -799,28 +799,30 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { onClickHideField={hideField} /> )} - - {logsVolumeEnabled && ( - onToggleLogsVolumeCollapse(true)} - /> - )} - +
+ + {logsVolumeEnabled && ( + onToggleLogsVolumeCollapse(true)} + /> + )} + +
Date: Fri, 3 Oct 2025 01:16:37 -0600 Subject: [PATCH 10/14] Provisioning: Refactor to combine validation and test endpoint logic (#111965) Provisioning: Refactor test endpoint --- apps/provisioning/pkg/repository/tester.go | 84 ++++++ .../pkg/repository/tester_test.go | 204 +++++++++++++++ .../pkg/repository/{test.go => validator.go} | 92 +++---- .../{test_test.go => validator_test.go} | 241 ++++-------------- pkg/operators/provisioning/repo_operator.go | 25 +- .../apis/provisioning/controller/health.go | 6 +- .../provisioning/controller/health_test.go | 22 +- pkg/registry/apis/provisioning/register.go | 43 +--- .../provisioning/register_validate_test.go | 3 +- pkg/registry/apis/provisioning/test.go | 9 +- 10 files changed, 437 insertions(+), 292 deletions(-) create mode 100644 apps/provisioning/pkg/repository/tester.go create mode 100644 apps/provisioning/pkg/repository/tester_test.go rename apps/provisioning/pkg/repository/{test.go => validator.go} (60%) rename apps/provisioning/pkg/repository/{test_test.go => validator_test.go} (64%) diff --git a/apps/provisioning/pkg/repository/tester.go b/apps/provisioning/pkg/repository/tester.go new file mode 100644 index 00000000000..6bd65443d92 --- /dev/null +++ b/apps/provisioning/pkg/repository/tester.go @@ -0,0 +1,84 @@ +package repository + +import ( + "context" + "net/http" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" +) + +// SimpleRepositoryTester will validate the repository configuration, and then proceed to test the connection to the repository +type SimpleRepositoryTester struct { + validator RepositoryValidator +} + +func NewSimpleRepositoryTester(validator RepositoryValidator) SimpleRepositoryTester { + return SimpleRepositoryTester{ + validator: validator, + } +} + +// TestRepository validates the repository and then runs a health check +func (t *SimpleRepositoryTester) TestRepository(ctx context.Context, repo Repository) (*provisioning.TestResults, error) { + errors := t.validator.ValidateRepository(repo) + if len(errors) > 0 { + rsp := &provisioning.TestResults{ + Code: http.StatusUnprocessableEntity, // Invalid + Success: false, + Errors: make([]provisioning.ErrorDetails, len(errors)), + } + for i, err := range errors { + rsp.Errors[i] = provisioning.ErrorDetails{ + Type: metav1.CauseType(err.Type), + Field: err.Field, + Detail: err.Detail, + } + } + return rsp, nil + } + + return repo.Test(ctx) +} + +type VerifyAgainstExistingRepositories func(ctx context.Context, cfg *provisioning.Repository) *field.Error // defined this way to prevent an import cycle + +// RepositoryTesterWithExistingChecker will validate the repository configuration, run a health check, and then compare it against existing repositories +type RepositoryTesterWithExistingChecker struct { + tester SimpleRepositoryTester + verify VerifyAgainstExistingRepositories +} + +func NewRepositoryTesterWithExistingChecker(tester SimpleRepositoryTester, verify VerifyAgainstExistingRepositories) RepositoryTesterWithExistingChecker { + return RepositoryTesterWithExistingChecker{ + tester: tester, + verify: verify, + } +} + +// TestRepositoryAndCheckExisting validates the repository, runs a health check, and then compares it against existing repositories +func (c *RepositoryTesterWithExistingChecker) TestRepositoryAndCheckExisting(ctx context.Context, repo Repository) (*provisioning.TestResults, error) { + rsp, err := c.tester.TestRepository(ctx, repo) + if err != nil { + return nil, err + } + + if rsp.Success { + cfg := repo.Config() + if validationErr := c.verify(ctx, cfg); validationErr != nil { + rsp = &provisioning.TestResults{ + Success: false, + Code: http.StatusUnprocessableEntity, + Errors: []provisioning.ErrorDetails{{ + Type: metav1.CauseType(validationErr.Type), + Field: validationErr.Field, + Detail: validationErr.Detail, + }}, + } + } + } + + return rsp, nil +} diff --git a/apps/provisioning/pkg/repository/tester_test.go b/apps/provisioning/pkg/repository/tester_test.go new file mode 100644 index 00000000000..91f29d15ceb --- /dev/null +++ b/apps/provisioning/pkg/repository/tester_test.go @@ -0,0 +1,204 @@ +package repository + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" +) + +func TestTestRepository(t *testing.T) { + tests := []struct { + name string + repository *MockRepository + expectedCode int + expectedErrs []provisioning.ErrorDetails + expectedError error + }{ + { + name: "validation fails", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + // Missing required title + }, + }) + m.On("Validate").Return(field.ErrorList{}) + return m + }(), + expectedCode: http.StatusUnprocessableEntity, + expectedErrs: []provisioning.ErrorDetails{{ + Type: metav1.CauseTypeFieldValueRequired, + Field: "spec.title", + Detail: "a repository title must be given", + }}, + }, + { + name: "test passes", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + }, + }) + m.On("Validate").Return(field.ErrorList{}) + m.On("Test", mock.Anything).Return(&provisioning.TestResults{ + Code: http.StatusOK, + Success: true, + }, nil) + return m + }(), + expectedCode: http.StatusOK, + expectedErrs: nil, + }, + { + name: "test fails with error", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + }, + }) + m.On("Validate").Return(field.ErrorList{}) + m.On("Test", mock.Anything).Return(nil, fmt.Errorf("test error")) + return m + }(), + expectedError: fmt.Errorf("test error"), + }, + { + name: "test fails with results", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + }, + }) + m.On("Validate").Return(field.ErrorList{}) + m.On("Test", mock.Anything).Return(&provisioning.TestResults{ + Code: http.StatusBadRequest, + Success: false, + Errors: []provisioning.ErrorDetails{{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "spec.property", + }}, + }, nil) + return m + }(), + expectedCode: http.StatusBadRequest, + expectedErrs: []provisioning.ErrorDetails{{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "spec.property", + }}, + }, + } + + tester := NewSimpleRepositoryTester(NewValidator(10*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true)) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + results, err := tester.TestRepository(context.Background(), tt.repository) + + if tt.expectedError != nil { + require.Error(t, err) + require.Equal(t, tt.expectedError.Error(), err.Error()) + return + } + + require.NoError(t, err) + require.NotNil(t, results) + require.Equal(t, tt.expectedCode, results.Code) + + if tt.expectedErrs != nil { + require.Equal(t, tt.expectedErrs, results.Errors) + require.False(t, results.Success) + } else { + require.True(t, results.Success) + require.Empty(t, results.Errors) + } + }) + } +} + +func TestTester_TestRepository(t *testing.T) { + repository := NewMockRepository(t) + repository.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + }, + }) + repository.On("Validate").Return(field.ErrorList{}) + repository.On("Test", mock.Anything).Return(&provisioning.TestResults{ + Code: http.StatusOK, + Success: true, + }, nil) + + tester := NewSimpleRepositoryTester(NewValidator(10*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true)) + results, err := tester.TestRepository(context.Background(), repository) + require.NoError(t, err) + require.NotNil(t, results) + require.Equal(t, http.StatusOK, results.Code) + require.True(t, results.Success) +} + +func TestFromFieldError(t *testing.T) { + tests := []struct { + name string + fieldError *field.Error + expectedCode int + expectedField string + expectedType metav1.CauseType + expectedDetail string + }{ + { + name: "required field error", + fieldError: &field.Error{ + Type: field.ErrorTypeRequired, + Field: "spec.title", + Detail: "a repository title must be given", + }, + expectedCode: http.StatusBadRequest, + expectedField: "spec.title", + expectedType: metav1.CauseTypeFieldValueRequired, + expectedDetail: "a repository title must be given", + }, + { + name: "not supported field error", + fieldError: &field.Error{ + Type: field.ErrorTypeNotSupported, + Field: "spec.workflow", + Detail: "branch is only supported on git repositories", + }, + expectedCode: http.StatusBadRequest, + expectedField: "spec.workflow", + expectedType: metav1.CauseTypeFieldValueNotSupported, + expectedDetail: "branch is only supported on git repositories", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FromFieldError(tt.fieldError) + + require.NotNil(t, result) + require.Equal(t, tt.expectedCode, result.Code) + require.False(t, result.Success) + require.Len(t, result.Errors, 1) + + errorDetail := result.Errors[0] + require.Equal(t, tt.expectedField, errorDetail.Field) + require.Equal(t, tt.expectedType, errorDetail.Type) + require.Equal(t, tt.expectedDetail, errorDetail.Detail) + }) + } +} diff --git a/apps/provisioning/pkg/repository/test.go b/apps/provisioning/pkg/repository/validator.go similarity index 60% rename from apps/provisioning/pkg/repository/test.go rename to apps/provisioning/pkg/repository/validator.go index 251c8a9f06c..73198f0d7ee 100644 --- a/apps/provisioning/pkg/repository/test.go +++ b/apps/provisioning/pkg/repository/validator.go @@ -1,10 +1,10 @@ package repository import ( - "context" "fmt" "net/http" "slices" + "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" @@ -12,57 +12,27 @@ import ( provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" ) -// RepositoryValidator interface for validating repositories against existing ones -type RepositoryValidator interface { - VerifyAgainstExistingRepositories(ctx context.Context, cfg *provisioning.Repository) *field.Error +type RepositoryValidator struct { + allowedTargets []provisioning.SyncTargetType + allowImageRendering bool + minSyncInterval time.Duration } -func TestRepository(ctx context.Context, repo Repository) (*provisioning.TestResults, error) { - return TestRepositoryWithValidator(ctx, repo, nil) +func NewValidator(minSyncInterval time.Duration, allowedTargets []provisioning.SyncTargetType, allowImageRendering bool) RepositoryValidator { + // do not allow minsync interval to be less than 10 + if minSyncInterval <= 10*time.Second { + minSyncInterval = 10 * time.Second + } + + return RepositoryValidator{ + allowedTargets: allowedTargets, + allowImageRendering: allowImageRendering, + minSyncInterval: minSyncInterval, + } } -func TestRepositoryWithValidator(ctx context.Context, repo Repository, validator RepositoryValidator) (*provisioning.TestResults, error) { - errors := ValidateRepository(repo) - if len(errors) > 0 { - rsp := &provisioning.TestResults{ - Code: http.StatusUnprocessableEntity, // Invalid - Success: false, - Errors: make([]provisioning.ErrorDetails, len(errors)), - } - for i, err := range errors { - rsp.Errors[i] = provisioning.ErrorDetails{ - Type: metav1.CauseType(err.Type), - Field: err.Field, - Detail: err.Detail, - } - } - return rsp, nil - } - - rsp, err := repo.Test(ctx) - if err != nil { - return nil, err - } - - if rsp.Success && validator != nil { - cfg := repo.Config() - if validationErr := validator.VerifyAgainstExistingRepositories(ctx, cfg); validationErr != nil { - rsp = &provisioning.TestResults{ - Success: false, - Code: http.StatusUnprocessableEntity, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseType(validationErr.Type), - Field: validationErr.Field, - Detail: validationErr.Detail, - }}, - } - } - } - - return rsp, nil -} - -func ValidateRepository(repo Repository) field.ErrorList { +// ValidateRepository solely does configuration checks on the repository object. It does not run a health check or compare against existing repositories. +func (v *RepositoryValidator) ValidateRepository(repo Repository) field.ErrorList { list := repo.Validate() cfg := repo.Config() @@ -70,9 +40,22 @@ func ValidateRepository(repo Repository) field.ErrorList { list = append(list, field.Required(field.NewPath("spec", "title"), "a repository title must be given")) } - if cfg.Spec.Sync.Enabled && cfg.Spec.Sync.Target == "" { - list = append(list, field.Required(field.NewPath("spec", "sync", "target"), - "The target type is required when sync is enabled")) + if cfg.Spec.Sync.Enabled { + if cfg.Spec.Sync.Target == "" { + list = append(list, field.Required(field.NewPath("spec", "sync", "target"), + "The target type is required when sync is enabled")) + } else if !slices.Contains(v.allowedTargets, cfg.Spec.Sync.Target) { + list = append(list, + field.Invalid( + field.NewPath("spec", "target"), + cfg.Spec.Sync.Target, + "sync target is not supported")) + } + + if cfg.Spec.Sync.IntervalSeconds < int64(v.minSyncInterval.Seconds()) { + list = append(list, field.Invalid(field.NewPath("spec", "sync", "intervalSeconds"), + cfg.Spec.Sync.IntervalSeconds, fmt.Sprintf("Interval must be at least %d seconds", int64(v.minSyncInterval.Seconds())))) + } } // Reserved names (for now) @@ -131,6 +114,13 @@ func ValidateRepository(repo Repository) field.ErrorList { } } + if !v.allowImageRendering && cfg.Spec.GitHub != nil && cfg.Spec.GitHub.GenerateDashboardPreviews { + list = append(list, + field.Invalid(field.NewPath("spec", "generateDashboardPreviews"), + cfg.Spec.GitHub.GenerateDashboardPreviews, + "image rendering is not enabled")) + } + return list } diff --git a/apps/provisioning/pkg/repository/test_test.go b/apps/provisioning/pkg/repository/validator_test.go similarity index 64% rename from apps/provisioning/pkg/repository/test_test.go rename to apps/provisioning/pkg/repository/validator_test.go index 4ebdd619d77..cb8b726a9bf 100644 --- a/apps/provisioning/pkg/repository/test_test.go +++ b/apps/provisioning/pkg/repository/validator_test.go @@ -1,12 +1,9 @@ package repository import ( - "context" - "fmt" - "net/http" "testing" + "time" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" @@ -74,6 +71,28 @@ func TestValidateRepository(t *testing.T) { require.Contains(t, errors.ToAggregate().Error(), "spec.sync.target: Required value") }, }, + { + name: "sync interval too low", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + Sync: provisioning.SyncOptions{ + Enabled: true, + Target: provisioning.SyncTargetTypeFolder, + IntervalSeconds: 5, + }, + }, + }) + m.On("Validate").Return(field.ErrorList{}) + return m + }(), + expectedErrs: 1, + validateError: func(t *testing.T, errors field.ErrorList) { + require.Contains(t, errors.ToAggregate().Error(), "spec.sync.intervalSeconds: Invalid value") + }, + }, { name: "reserved name", repository: func() *MockRepository { @@ -132,6 +151,27 @@ func TestValidateRepository(t *testing.T) { require.Contains(t, errors.ToAggregate().Error(), "spec.github: Invalid value") }, }, + { + name: "github enabled when image rendering is not allowed", + repository: func() *MockRepository { + m := NewMockRepository(t) + m.On("Config").Return(&provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Title: "Test Repo", + Type: provisioning.GitHubRepositoryType, + GitHub: &provisioning.GitHubRepositoryConfig{ + GenerateDashboardPreviews: true, + }, + }, + }) + m.On("Validate").Return(field.ErrorList{}) + return m + }(), + expectedErrs: 1, + validateError: func(t *testing.T, errors field.ErrorList) { + require.Contains(t, errors.ToAggregate().Error(), "spec.generateDashboardPreviews: Invalid value") + }, + }, { name: "mismatched git config", repository: func() *MockRepository { @@ -163,16 +203,18 @@ func TestValidateRepository(t *testing.T) { Sync: provisioning.SyncOptions{ Enabled: true, IntervalSeconds: 5, + Target: provisioning.SyncTargetTypeInstance, }, }, }) m.On("Validate").Return(field.ErrorList{}) return m }(), - expectedErrs: 3, + expectedErrs: 4, // 1. missing title // 2. sync target missing // 3. reserved name + // 4. sync target not supported }, { name: "branch workflow for non-github repository", @@ -258,9 +300,10 @@ func TestValidateRepository(t *testing.T) { }, } + validator := NewValidator(10*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder}, false) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - errors := ValidateRepository(tt.repository) + errors := validator.ValidateRepository(tt.repository) require.Len(t, errors, tt.expectedErrs) if tt.validateError != nil { tt.validateError(t, errors) @@ -268,189 +311,3 @@ func TestValidateRepository(t *testing.T) { }) } } - -func TestTestRepository(t *testing.T) { - tests := []struct { - name string - repository *MockRepository - expectedCode int - expectedErrs []provisioning.ErrorDetails - expectedError error - }{ - { - name: "validation fails", - repository: func() *MockRepository { - m := NewMockRepository(t) - m.On("Config").Return(&provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - // Missing required title - }, - }) - m.On("Validate").Return(field.ErrorList{}) - return m - }(), - expectedCode: http.StatusUnprocessableEntity, - expectedErrs: []provisioning.ErrorDetails{{ - Type: metav1.CauseTypeFieldValueRequired, - Field: "spec.title", - Detail: "a repository title must be given", - }}, - }, - { - name: "test passes", - repository: func() *MockRepository { - m := NewMockRepository(t) - m.On("Config").Return(&provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Title: "Test Repo", - }, - }) - m.On("Validate").Return(field.ErrorList{}) - m.On("Test", mock.Anything).Return(&provisioning.TestResults{ - Code: http.StatusOK, - Success: true, - }, nil) - return m - }(), - expectedCode: http.StatusOK, - expectedErrs: nil, - }, - { - name: "test fails with error", - repository: func() *MockRepository { - m := NewMockRepository(t) - m.On("Config").Return(&provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Title: "Test Repo", - }, - }) - m.On("Validate").Return(field.ErrorList{}) - m.On("Test", mock.Anything).Return(nil, fmt.Errorf("test error")) - return m - }(), - expectedError: fmt.Errorf("test error"), - }, - { - name: "test fails with results", - repository: func() *MockRepository { - m := NewMockRepository(t) - m.On("Config").Return(&provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Title: "Test Repo", - }, - }) - m.On("Validate").Return(field.ErrorList{}) - m.On("Test", mock.Anything).Return(&provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []provisioning.ErrorDetails{{ - Type: metav1.CauseTypeFieldValueInvalid, - Field: "spec.property", - }}, - }, nil) - return m - }(), - expectedCode: http.StatusBadRequest, - expectedErrs: []provisioning.ErrorDetails{{ - Type: metav1.CauseTypeFieldValueInvalid, - Field: "spec.property", - }}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - results, err := TestRepository(context.Background(), tt.repository) - - if tt.expectedError != nil { - require.Error(t, err) - require.Equal(t, tt.expectedError.Error(), err.Error()) - return - } - - require.NoError(t, err) - require.NotNil(t, results) - require.Equal(t, tt.expectedCode, results.Code) - - if tt.expectedErrs != nil { - require.Equal(t, tt.expectedErrs, results.Errors) - require.False(t, results.Success) - } else { - require.True(t, results.Success) - require.Empty(t, results.Errors) - } - }) - } -} - -func TestTester_TestRepository(t *testing.T) { - repository := NewMockRepository(t) - repository.On("Config").Return(&provisioning.Repository{ - Spec: provisioning.RepositorySpec{ - Title: "Test Repo", - }, - }) - repository.On("Validate").Return(field.ErrorList{}) - repository.On("Test", mock.Anything).Return(&provisioning.TestResults{ - Code: http.StatusOK, - Success: true, - }, nil) - - results, err := TestRepository(context.Background(), repository) - require.NoError(t, err) - require.NotNil(t, results) - require.Equal(t, http.StatusOK, results.Code) - require.True(t, results.Success) -} - -func TestFromFieldError(t *testing.T) { - tests := []struct { - name string - fieldError *field.Error - expectedCode int - expectedField string - expectedType metav1.CauseType - expectedDetail string - }{ - { - name: "required field error", - fieldError: &field.Error{ - Type: field.ErrorTypeRequired, - Field: "spec.title", - Detail: "a repository title must be given", - }, - expectedCode: http.StatusBadRequest, - expectedField: "spec.title", - expectedType: metav1.CauseTypeFieldValueRequired, - expectedDetail: "a repository title must be given", - }, - { - name: "not supported field error", - fieldError: &field.Error{ - Type: field.ErrorTypeNotSupported, - Field: "spec.workflow", - Detail: "branch is only supported on git repositories", - }, - expectedCode: http.StatusBadRequest, - expectedField: "spec.workflow", - expectedType: metav1.CauseTypeFieldValueNotSupported, - expectedDetail: "branch is only supported on git repositories", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := FromFieldError(tt.fieldError) - - require.NotNil(t, result) - require.Equal(t, tt.expectedCode, result.Code) - require.False(t, result.Success) - require.Len(t, result.Errors, 1) - - errorDetail := result.Errors[0] - require.Equal(t, tt.expectedField, errorDetail.Field) - require.Equal(t, tt.expectedType, errorDetail.Type) - require.Equal(t, tt.expectedDetail, errorDetail.Detail) - }) - } -} diff --git a/pkg/operators/provisioning/repo_operator.go b/pkg/operators/provisioning/repo_operator.go index 0ec243bb41e..f96653a1d87 100644 --- a/pkg/operators/provisioning/repo_operator.go +++ b/pkg/operators/provisioning/repo_operator.go @@ -11,9 +11,11 @@ import ( "github.com/grafana/grafana-app-sdk/logging" appcontroller "github.com/grafana/grafana/apps/provisioning/pkg/controller" + "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/prometheus/client_golang/prometheus" "k8s.io/client-go/tools/cache" + "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" @@ -66,8 +68,14 @@ func RunRepoController(deps server.OperatorDependencies) error { if err != nil { return fmt.Errorf("create API client job store: %w", err) } + + allowedTargets := []v0alpha1.SyncTargetType{} + for _, target := range controllerCfg.allowedTargets { + allowedTargets = append(allowedTargets, v0alpha1.SyncTargetType(target)) + } + validator := repository.NewValidator(controllerCfg.minSyncInterval, allowedTargets, controllerCfg.allowImageRendering) statusPatcher := appcontroller.NewRepositoryStatusPatcher(controllerCfg.provisioningClient.ProvisioningV0alpha1()) - healthChecker := controller.NewHealthChecker(statusPatcher, deps.Registerer) + healthChecker := controller.NewHealthChecker(statusPatcher, deps.Registerer, repository.NewSimpleRepositoryTester(validator)) repoInformer := informerFactory.Provisioning().V0alpha1().Repositories() controller, err := controller.NewRepositoryController( @@ -98,7 +106,10 @@ func RunRepoController(deps server.OperatorDependencies) error { type repoControllerConfig struct { provisioningControllerConfig - workerCount int + workerCount int + allowedTargets []string + allowImageRendering bool + minSyncInterval time.Duration } func getRepoControllerConfig(cfg *setting.Cfg, registry prometheus.Registerer) (*repoControllerConfig, error) { @@ -106,8 +117,18 @@ func getRepoControllerConfig(cfg *setting.Cfg, registry prometheus.Registerer) ( if err != nil { return nil, err } + + allowedTargets := []string{} + cfg.SectionWithEnvOverrides("provisioning").Key("allowed_targets").Strings("|") + if len(allowedTargets) == 0 { + allowedTargets = []string{"folder"} + } + return &repoControllerConfig{ provisioningControllerConfig: *controllerCfg, + allowedTargets: allowedTargets, workerCount: cfg.SectionWithEnvOverrides("operator").Key("worker_count").MustInt(1), + allowImageRendering: cfg.SectionWithEnvOverrides("provisioning").Key("allow_image_rendering").MustBool(false), + minSyncInterval: cfg.SectionWithEnvOverrides("provisioning").Key("min_sync_interval").MustDuration(1 * time.Minute), }, nil } diff --git a/pkg/registry/apis/provisioning/controller/health.go b/pkg/registry/apis/provisioning/controller/health.go index cf3aeedc36d..5e5953b4117 100644 --- a/pkg/registry/apis/provisioning/controller/health.go +++ b/pkg/registry/apis/provisioning/controller/health.go @@ -23,14 +23,16 @@ type StatusPatcher interface { type HealthChecker struct { statusPatcher StatusPatcher healthMetrics healthMetrics + tester repository.SimpleRepositoryTester } // NewHealthChecker creates a new health checker -func NewHealthChecker(statusPatcher StatusPatcher, registry prometheus.Registerer) *HealthChecker { +func NewHealthChecker(statusPatcher StatusPatcher, registry prometheus.Registerer, tester repository.SimpleRepositoryTester) *HealthChecker { healthMetrics := registerHealthMetrics(registry) return &HealthChecker{ statusPatcher: statusPatcher, healthMetrics: healthMetrics, + tester: tester, } } @@ -176,7 +178,7 @@ func (hc *HealthChecker) refreshHealth(ctx context.Context, repo repository.Repo hc.healthMetrics.RecordHealthCheck(outcome, time.Since(start).Seconds()) }() - res, err := repository.TestRepository(ctx, repo) + res, err := hc.tester.TestRepository(ctx, repo) if err != nil { outcome = utils.ErrorOutcome logger.Error("failed to test repository", "error", err) diff --git a/pkg/registry/apis/provisioning/controller/health_test.go b/pkg/registry/apis/provisioning/controller/health_test.go index 74f8a127be6..722500d5a94 100644 --- a/pkg/registry/apis/provisioning/controller/health_test.go +++ b/pkg/registry/apis/provisioning/controller/health_test.go @@ -13,13 +13,15 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + repository "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/controller/mocks" ) func TestNewHealthChecker(t *testing.T) { mockPatcher := mocks.NewStatusPatcher(t) - hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) + validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator)) assert.NotNil(t, hc) assert.Equal(t, mockPatcher, hc.statusPatcher) @@ -136,7 +138,8 @@ func TestShouldCheckHealth(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockPatcher := mocks.NewStatusPatcher(t) - hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) + validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator)) result := hc.ShouldCheckHealth(tt.repo) assert.Equal(t, tt.expected, result) @@ -223,7 +226,8 @@ func TestHasRecentFailure(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockPatcher := mocks.NewStatusPatcher(t) - hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) + validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator)) result := hc.HasRecentFailure(tt.healthStatus, tt.failureType) assert.Equal(t, tt.expected, result) @@ -265,7 +269,8 @@ func TestRecordFailure(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockPatcher := mocks.NewStatusPatcher(t) - hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) + validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator)) repo := &provisioning.Repository{ Status: provisioning.RepositoryStatus{ @@ -310,7 +315,8 @@ func TestRecordFailure(t *testing.T) { func TestRecordFailureFunction(t *testing.T) { mockPatcher := mocks.NewStatusPatcher(t) - hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) + validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator)) testErr := errors.New("test error") result := hc.recordFailure(provisioning.HealthFailureHook, testErr) @@ -447,7 +453,8 @@ func TestRefreshHealth(t *testing.T) { testError: tt.testError, } - hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) + validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator)) if tt.expectPatch { if tt.patchError != nil { @@ -557,7 +564,8 @@ func TestHasHealthStatusChanged(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockPatcher := mocks.NewStatusPatcher(t) - hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) + validator := repository.NewValidator(30*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder, provisioning.SyncTargetTypeInstance}, true) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry(), repository.NewSimpleRepositoryTester(validator)) result := hc.hasHealthStatusChanged(tt.old, tt.new) assert.Equal(t, tt.expected, result) diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index 5590cbeafe5..2aeb926be21 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "net/url" - "slices" "strings" "time" @@ -92,7 +91,6 @@ type APIBuilder struct { allowedTargets []provisioning.SyncTargetType allowImageRendering bool - minSyncInterval time.Duration features featuremgmt.FeatureToggles usageStats usagestats.Service @@ -117,6 +115,7 @@ type APIBuilder struct { access authlib.AccessChecker statusPatcher *appcontroller.RepositoryStatusPatcher healthChecker *controller.HealthChecker + validator repository.RepositoryValidator // Extras provides additional functionality to the API. extras []Extra extraWorkers []jobs.Worker @@ -158,11 +157,6 @@ func NewAPIBuilder( parsers := resources.NewParserFactory(clients) resourceLister := resources.NewResourceListerForMigrations(unified, legacyMigrator, storageStatus) - // do not allow minsync interval to be less than 10 - if minSyncInterval <= 10*time.Second { - minSyncInterval = 10 * time.Second - } - b := &APIBuilder{ onlyApiServer: onlyApiServer, tracer: tracer, @@ -179,11 +173,11 @@ func NewAPIBuilder( access: access, jobHistoryConfig: jobHistoryConfig, extraWorkers: extraWorkers, - allowedTargets: allowedTargets, restConfigGetter: restConfigGetter, + allowedTargets: allowedTargets, allowImageRendering: allowImageRendering, - minSyncInterval: minSyncInterval, registry: registry, + validator: repository.NewValidator(minSyncInterval, allowedTargets, allowImageRendering), } for _, builder := range extraBuilders { @@ -484,7 +478,7 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI storage[provisioning.RepositoryResourceInfo.StoragePath("status")] = repositoryStatusStorage // TODO: Add some logic so that the connectors can registered themselves and we don't have logic all over the place - storage[provisioning.RepositoryResourceInfo.StoragePath("test")] = NewTestConnector(b) + storage[provisioning.RepositoryResourceInfo.StoragePath("test")] = NewTestConnector(b, repository.NewRepositoryTesterWithExistingChecker(repository.NewSimpleRepositoryTester(b.validator), b.VerifyAgainstExistingRepositories)) storage[provisioning.RepositoryResourceInfo.StoragePath("files")] = NewFilesConnector(b, b.parsers, b.clients, b.access) storage[provisioning.RepositoryResourceInfo.StoragePath("refs")] = NewRefsConnector(b) storage[provisioning.RepositoryResourceInfo.StoragePath("resources")] = &listConnector{ @@ -585,29 +579,14 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm return err } - list := repository.ValidateRepository(repo) + // ALL configuration validations should be done in ValidateRepository - + // this is how the UI is able to show proper validation errors + // + // the only time to add configuration checks here is if you need to compare + // the incoming change to the current configuration + list := b.validator.ValidateRepository(repo) cfg := repo.Config() - if !slices.Contains(b.allowedTargets, cfg.Spec.Sync.Target) { - list = append(list, - field.Invalid( - field.NewPath("spec", "target"), - cfg.Spec.Sync.Target, - "sync target is not supported")) - } - - if cfg.Spec.Sync.Enabled && cfg.Spec.Sync.IntervalSeconds < int64(b.minSyncInterval.Seconds()) { - list = append(list, field.Invalid(field.NewPath("spec", "sync", "intervalSeconds"), - cfg.Spec.Sync.IntervalSeconds, fmt.Sprintf("Interval must be at least %d seconds", int64(b.minSyncInterval.Seconds())))) - } - - if !b.allowImageRendering && cfg.Spec.GitHub != nil && cfg.Spec.GitHub.GenerateDashboardPreviews { - list = append(list, - field.Invalid(field.NewPath("spec", "generateDashboardPreviews"), - cfg.Spec.GitHub.GenerateDashboardPreviews, - "image rendering is not enabled")) - } - if a.GetOperation() == admission.Update { oldRepo, err := b.asRepository(ctx, a.GetOldObject(), nil) if err != nil { @@ -683,7 +662,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH } b.statusPatcher = appcontroller.NewRepositoryStatusPatcher(b.GetClient()) - b.healthChecker = controller.NewHealthChecker(b.statusPatcher, b.registry) + b.healthChecker = controller.NewHealthChecker(b.statusPatcher, b.registry, repository.NewSimpleRepositoryTester(b.validator)) // if running solely CRUD, skip the rest of the setup if b.onlyApiServer { diff --git a/pkg/registry/apis/provisioning/register_validate_test.go b/pkg/registry/apis/provisioning/register_validate_test.go index 0834b534cc0..18b366e4de8 100644 --- a/pkg/registry/apis/provisioning/register_validate_test.go +++ b/pkg/registry/apis/provisioning/register_validate_test.go @@ -23,11 +23,12 @@ func TestAPIBuilderValidate(t *testing.T) { mockRepo := repository.NewMockConfigRepository(t) mockRepo.EXPECT().Validate().Return(nil) factory.EXPECT().Build(mock.Anything, mock.Anything).Return(mockRepo, nil) + validator := repository.NewValidator(30*time.Second, []v0alpha1.SyncTargetType{v0alpha1.SyncTargetTypeFolder}, false) b := &APIBuilder{ repoFactory: factory, allowedTargets: []v0alpha1.SyncTargetType{v0alpha1.SyncTargetTypeFolder}, allowImageRendering: false, - minSyncInterval: 30 * time.Second, + validator: validator, } t.Run("min sync interval is less than 10 seconds", func(t *testing.T) { diff --git a/pkg/registry/apis/provisioning/test.go b/pkg/registry/apis/provisioning/test.go index b664e9e9039..43e28bdb334 100644 --- a/pkg/registry/apis/provisioning/test.go +++ b/pkg/registry/apis/provisioning/test.go @@ -31,7 +31,6 @@ type HealthCheckerProvider interface { type ConnectorDependencies interface { RepoGetter HealthCheckerProvider - repository.RepositoryValidator GetRepoFactory() repository.Factory } @@ -39,15 +38,15 @@ type testConnector struct { getter RepoGetter factory repository.Factory healthProvider HealthCheckerProvider - validator repository.RepositoryValidator + tester repository.RepositoryTesterWithExistingChecker } -func NewTestConnector(deps ConnectorDependencies) *testConnector { +func NewTestConnector(deps ConnectorDependencies, tester repository.RepositoryTesterWithExistingChecker) *testConnector { return &testConnector{ factory: deps.GetRepoFactory(), getter: deps, healthProvider: deps, - validator: deps, + tester: tester, } } @@ -186,7 +185,7 @@ func (s *testConnector) Connect(ctx context.Context, name string, opts runtime.O } } else { // Testing temporary repository - just run test without status update - rsp, err = repository.TestRepositoryWithValidator(ctx, repo, s.validator) + rsp, err = s.tester.TestRepositoryAndCheckExisting(ctx, repo) if err != nil { responder.Error(err) return From 6248971d1e3a225bbad6aca127e6ae3b18c6a319 Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Fri, 3 Oct 2025 11:08:14 +0200 Subject: [PATCH 11/14] docs(alerting): add settings for alert evaluation backoff retries (#111891) * docs(alerting): add settings for alert evaluation backoff retries * docs(alerting): add mention of backoff settings on the Error state docs * fix vale prose --- .../nodata-and-error-states.md | 6 ++-- .../setup-grafana/configure-grafana/_index.md | 35 ++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/docs/sources/alerting/fundamentals/alert-rule-evaluation/nodata-and-error-states.md b/docs/sources/alerting/fundamentals/alert-rule-evaluation/nodata-and-error-states.md index f91f9d9f000..50182bd520a 100644 --- a/docs/sources/alerting/fundamentals/alert-rule-evaluation/nodata-and-error-states.md +++ b/docs/sources/alerting/fundamentals/alert-rule-evaluation/nodata-and-error-states.md @@ -106,7 +106,7 @@ A Grafana-managed alert instance can be in any of the following states, dependin The **Error** state is triggered when the alert rule fails to evaluate its query or queries successfully. -This can occur due to evaluation timeouts (default: `30s`) or three repeated failures when querying the data source. The [`evaluation_timeout`](ref:evaluation_timeout) and [`max_attempts`](ref:max_attempts) options control these settings. +This can occur due to evaluation timeouts (default: `30s`) or repeated failures (default: `3`) when querying the data source. The [`evaluation_timeout`](ref:evaluation_timeout) and [`max_attempts`](ref:max_attempts) options control these settings. When an alert instance enters the **Error** state, Grafana, by default, triggers a new [`DatasourceError` alert](#no-data-and-error-alerts). You can control this behavior based on the desired outcome of your alert rule in [Modify the `No Data` or `Error` state](#modify-the-no-data-or-error-state). @@ -157,10 +157,10 @@ To minimize the number of **No Data** or **Error** state alerts received, try th To minimize timeouts resulting in the **Error** state, reduce the time range to request less data every evaluation cycle. -1. Change the default [evaluation time out](ref:evaluation_timeout). The default is set at 30 seconds. To increase the default evaluation timeout, open a support ticket from the [Cloud Portal](https://grafana.com/docs/grafana-cloud/account-management/support/#grafana-cloud-support-options). Note that this should be a last resort, because it may affect the performance of all alert rules and cause missed evaluations if the timeout is too long. - 1. To reduce multiple notifications from **Error** alerts, define a [notification policy](ref:notification-policies) to handle all related alerts with `alertname=DatasourceError`, and filter and group errors from the same data source using the `datasource_uid` label. +1. Change the [evaluation timeout](ref:evaluation_timeout) (default: `30s`) or the [retry mechanism (`max_attempts`)](ref:max_attempts) settings. This should be a last resort, as it can affect the performance of all alert rules and may cause missed evaluations if the timeout is too long. For Grafana Cloud, open a support ticket from the [Cloud Portal](https://grafana.com/docs/grafana-cloud/account-management/support/#grafana-cloud-support-options). + {{< admonition type="tip" >}} For common examples and practical guidance on handling **Error**, **No Data**, and **stale** alert scenarios, refer to the [Handle connectivity errors](ref:guide-connectivity-errors) and [Handle missing data](ref:guide-missing-data) guides. {{< /admonition >}} diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index a353e49f048..9702851cd19 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1915,7 +1915,40 @@ The timeout string is a possibly signed sequence of decimal numbers, followed by #### `max_attempts` -Sets a maximum number of times Grafana attempts to evaluate an alert rule before giving up on that evaluation. The default value is `3`. +The maximum number of times Grafana retries evaluating an alert rule before giving up on that evaluation. Default is `3`. + +The retry mechanism: + +- Adds jitter to retry delays to prevent thundering herd problems when multiple rules fail simultaneously. +- Stops when either `max_attempts` is reached or the rule’s evaluation interval is exceeded. + +You can customize retry behaviour with `initial_retry_delay`, `max_retry_delay`, and `randomization_factor`. + +#### `initial_retry_delay` + +The initial delay before retrying a failed alert evaluation. Default is `1s`. + +This value is the starting point for exponential backoff. + +#### `max_retry_delay` + +The maximum delay between retries during exponential backoff. Default is `4s`. + +After the retry delay reaches `max_retry_delay`, all subsequent retries use this delay. + +To avoid overlapping retries with scheduled evaluations, `max_retry_delay` must be less than the rule’s evaluation interval. + +#### `randomization_factor` + +The randomization factor for exponential backoff retries. Default is `0.1`. + +The value must be between `0` and `1`. + +The actual retry delay is chosen randomly between: + +``` +[current_delay*(1-randomization_factor), current_delay*(1+randomization_factor)] +``` #### `min_interval` From 76d467f2852de5593096765c76e06cc846ddaee7 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 3 Oct 2025 12:35:18 +0300 Subject: [PATCH 12/14] API clients: Automatically set PATCH headers (#111879) * API clients: Automatically set PATCH headers * Only modify /apis/ endpoints * Do not overwrite existing content-type --- .../app/api/clients/folder/v1beta1/index.ts | 20 +------------------ public/app/api/createBaseQuery.ts | 14 +++++++++++++ public/app/core/utils/fetch.ts | 4 +++- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/public/app/api/clients/folder/v1beta1/index.ts b/public/app/api/clients/folder/v1beta1/index.ts index cefd69c4955..bb0e11f218f 100644 --- a/public/app/api/clients/folder/v1beta1/index.ts +++ b/public/app/api/clients/folder/v1beta1/index.ts @@ -24,24 +24,6 @@ export const folderAPIv1beta1 = generatedAPI // We don't want delete to invalidate getFolder tags, as that would lead to unnecessary 404s invalidatesTags: (result, error) => (error ? [] : [{ type: 'Folder', id: 'LIST' }]), }, - updateFolder: { - query: (queryArg) => ({ - url: `/folders/${queryArg.name}`, - method: 'PATCH', - // We need to stringify the body and set the correct header for the call to work with k8s api. - body: JSON.stringify(queryArg.patch), - headers: { - 'Content-Type': 'application/strategic-merge-patch+json', - }, - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - fieldManager: queryArg.fieldManager, - fieldValidation: queryArg.fieldValidation, - force: queryArg.force, - }, - }), - }, }, }) .injectEndpoints({ @@ -98,4 +80,4 @@ export const { } = folderAPIv1beta1; // eslint-disable-next-line no-barrel-files/no-barrel-files -export { type Folder, type FolderList, type CreateFolderApiArg, type ReplaceFolderApiArg } from './endpoints.gen'; +export { type CreateFolderApiArg, type Folder, type FolderList, type ReplaceFolderApiArg } from './endpoints.gen'; diff --git a/public/app/api/createBaseQuery.ts b/public/app/api/createBaseQuery.ts index ac282ddc192..51037493e88 100644 --- a/public/app/api/createBaseQuery.ts +++ b/public/app/api/createBaseQuery.ts @@ -15,12 +15,26 @@ interface CreateBaseQueryOptions { export function createBaseQuery({ baseURL }: CreateBaseQueryOptions): BaseQueryFn { async function backendSrvBaseQuery(requestOptions: RequestOptions) { try { + const headers: Record = { + ...requestOptions.headers, + }; + + // Add Content-Type header for PATCH requests to /apis/ endpoints if not already set + if ( + requestOptions.method?.toUpperCase() === 'PATCH' && + baseURL?.startsWith('/apis/') && + !headers['Content-Type'] + ) { + headers['Content-Type'] = 'application/strategic-merge-patch+json'; + } + const { data: responseData, ...meta } = await lastValueFrom( getBackendSrv().fetch({ ...requestOptions, url: baseURL + requestOptions.url, showErrorAlert: requestOptions.showErrorAlert ?? false, data: requestOptions.body, + headers, }) ); return { data: responseData, meta }; diff --git a/public/app/core/utils/fetch.ts b/public/app/core/utils/fetch.ts index e714e944dc9..5be200a2eee 100644 --- a/public/app/core/utils/fetch.ts +++ b/public/app/core/utils/fetch.ts @@ -91,7 +91,9 @@ export const isContentTypeJson = (headers: Headers) => { const contentType = headers.get('content-type'); if ( contentType && - (contentType.toLowerCase() === 'application/json' || contentType.toLowerCase() === 'application/merge-patch+json') + ['application/json', 'application/merge-patch+json', 'application/strategic-merge-patch+json'].includes( + contentType.toLowerCase() + ) ) { return true; } From 0f60e2208e4d5ccbf1e8a8c49f5e2808516bf915 Mon Sep 17 00:00:00 2001 From: Mihai Doarna Date: Fri, 3 Oct 2025 12:48:38 +0300 Subject: [PATCH 13/14] IAM: Implement update method in team API (#111660) * implement team update in legacy store * add unit tests * add integration test * set permissions for user in integration tests * add more integration tests for update * update validations * add unit tests for ValidateOnUpdate() func * fix integration test --- pkg/registry/apis/iam/legacy/sql.go | 1 + pkg/registry/apis/iam/legacy/sql_test.go | 19 ++ pkg/registry/apis/iam/legacy/team.go | 85 +++++++ .../mysql--update_team-update_team_basic.sql | 7 + ...ostgres--update_team-update_team_basic.sql | 7 + .../sqlite--update_team-update_team_basic.sql | 7 + pkg/registry/apis/iam/legacy/update_team.sql | 7 + pkg/registry/apis/iam/register.go | 7 + pkg/registry/apis/iam/team/store.go | 47 +++- pkg/registry/apis/iam/team/validate.go | 25 +++ pkg/registry/apis/iam/team/validate_test.go | 210 ++++++++++++++++++ pkg/tests/apis/iam/team_integration_test.go | 62 +++++- .../iam/testdata/team-test-update-v0.yaml | 7 + 13 files changed, 484 insertions(+), 7 deletions(-) create mode 100755 pkg/registry/apis/iam/legacy/testdata/mysql--update_team-update_team_basic.sql create mode 100755 pkg/registry/apis/iam/legacy/testdata/postgres--update_team-update_team_basic.sql create mode 100755 pkg/registry/apis/iam/legacy/testdata/sqlite--update_team-update_team_basic.sql create mode 100644 pkg/registry/apis/iam/legacy/update_team.sql create mode 100644 pkg/tests/apis/iam/testdata/team-test-update-v0.yaml diff --git a/pkg/registry/apis/iam/legacy/sql.go b/pkg/registry/apis/iam/legacy/sql.go index 3f2156d26a2..58d7c9f1fd3 100644 --- a/pkg/registry/apis/iam/legacy/sql.go +++ b/pkg/registry/apis/iam/legacy/sql.go @@ -31,6 +31,7 @@ type LegacyIdentityStore interface { GetTeamInternalID(ctx context.Context, ns claims.NamespaceInfo, query GetTeamInternalIDQuery) (*GetTeamInternalIDResult, error) CreateTeam(ctx context.Context, ns claims.NamespaceInfo, cmd CreateTeamCommand) (*CreateTeamResult, error) + UpdateTeam(ctx context.Context, ns claims.NamespaceInfo, cmd UpdateTeamCommand) (*UpdateTeamResult, error) ListTeams(ctx context.Context, ns claims.NamespaceInfo, query ListTeamQuery) (*ListTeamResult, error) DeleteTeam(ctx context.Context, ns claims.NamespaceInfo, cmd DeleteTeamCommand) error ListTeamBindings(ctx context.Context, ns claims.NamespaceInfo, query ListTeamBindingsQuery) (*ListTeamBindingsResult, error) diff --git a/pkg/registry/apis/iam/legacy/sql_test.go b/pkg/registry/apis/iam/legacy/sql_test.go index d9187c163b5..7c4fad74ba8 100644 --- a/pkg/registry/apis/iam/legacy/sql_test.go +++ b/pkg/registry/apis/iam/legacy/sql_test.go @@ -60,6 +60,12 @@ func TestIdentityQueries(t *testing.T) { return &v } + updateTeam := func(cmd *UpdateTeamCommand) sqltemplate.SQLTemplate { + v := newUpdateTeam(nodb, cmd) + v.SQLTemplate = mocks.NewTestingSQLTemplate() + return &v + } + listTeams := func(q *ListTeamQuery) sqltemplate.SQLTemplate { v := newListTeams(nodb, q) v.SQLTemplate = mocks.NewTestingSQLTemplate() @@ -393,6 +399,19 @@ func TestIdentityQueries(t *testing.T) { }), }, }, + sqlUpdateTeamTemplate: { + { + Name: "update_team_basic", + Data: updateTeam(&UpdateTeamCommand{ + UID: "team-1", + Name: "Team 1", + Email: "team1@example.com", + IsProvisioned: true, + ExternalUID: "team-1-uid", + Updated: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)), + }), + }, + }, sqlDeleteTeamTemplate: { { Name: "delete_team_basic", diff --git a/pkg/registry/apis/iam/legacy/team.go b/pkg/registry/apis/iam/legacy/team.go index a52f7658df9..0ad11584a7f 100644 --- a/pkg/registry/apis/iam/legacy/team.go +++ b/pkg/registry/apis/iam/legacy/team.go @@ -265,6 +265,91 @@ func (s *legacySQLStore) CreateTeam(ctx context.Context, ns claims.NamespaceInfo return &CreateTeamResult{Team: createdTeam}, nil } +type UpdateTeamCommand struct { + UID string + Name string + Updated DBTime + Email string + ExternalID string + IsProvisioned bool + ExternalUID string +} + +type UpdateTeamResult struct { + Team team.Team +} + +var sqlUpdateTeamTemplate = mustTemplate("update_team.sql") + +func newUpdateTeam(sql *legacysql.LegacyDatabaseHelper, cmd *UpdateTeamCommand) updateTeamQuery { + return updateTeamQuery{ + SQLTemplate: sqltemplate.New(sql.DialectForDriver()), + TeamTable: sql.Table("team"), + Command: cmd, + } +} + +type updateTeamQuery struct { + sqltemplate.SQLTemplate + TeamTable string + Command *UpdateTeamCommand +} + +func (r updateTeamQuery) Validate() error { + return nil +} + +func (s *legacySQLStore) UpdateTeam(ctx context.Context, ns claims.NamespaceInfo, cmd UpdateTeamCommand) (*UpdateTeamResult, error) { + now := time.Now().UTC().Truncate(time.Second) + + cmd.Updated = NewDBTime(now) + + sql, err := s.sql(ctx) + if err != nil { + return nil, err + } + + req := newUpdateTeam(sql, &cmd) + + var updatedTeam team.Team + err = sql.DB.GetSqlxSession().WithTransaction(ctx, func(st *session.SessionTx) error { + _, err := s.GetTeamInternalID(ctx, ns, GetTeamInternalIDQuery{ + OrgID: ns.OrgID, + UID: cmd.UID, + }) + if err != nil { + return fmt.Errorf("team not found: %w", err) + } + + teamQuery, err := sqltemplate.Execute(sqlUpdateTeamTemplate, req) + if err != nil { + return fmt.Errorf("failed to execute team update template %q: %w", sqlUpdateTeamTemplate.Name(), err) + } + + _, err = st.Exec(ctx, teamQuery, req.GetArgs()...) + if err != nil { + return fmt.Errorf("failed to update team: %w", err) + } + + updatedTeam = team.Team{ + UID: cmd.UID, + Name: cmd.Name, + Email: cmd.Email, + ExternalUID: cmd.ExternalUID, + IsProvisioned: cmd.IsProvisioned, + Updated: cmd.Updated.Time, + } + + return nil + }) + + if err != nil { + return nil, err + } + + return &UpdateTeamResult{Team: updatedTeam}, nil +} + type DeleteTeamCommand struct { UID string } diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--update_team-update_team_basic.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--update_team-update_team_basic.sql new file mode 100755 index 00000000000..3d0c3bf0520 --- /dev/null +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--update_team-update_team_basic.sql @@ -0,0 +1,7 @@ +UPDATE `grafana`.`team` +SET name = 'Team 1', + updated = '2023-01-01 12:00:00', + email = 'team1@example.com', + is_provisioned = TRUE, + external_uid = 'team-1-uid' +WHERE uid = 'team-1' diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--update_team-update_team_basic.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--update_team-update_team_basic.sql new file mode 100755 index 00000000000..7ec26ef6c6a --- /dev/null +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--update_team-update_team_basic.sql @@ -0,0 +1,7 @@ +UPDATE "grafana"."team" +SET name = 'Team 1', + updated = '2023-01-01 12:00:00', + email = 'team1@example.com', + is_provisioned = TRUE, + external_uid = 'team-1-uid' +WHERE uid = 'team-1' diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--update_team-update_team_basic.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--update_team-update_team_basic.sql new file mode 100755 index 00000000000..7ec26ef6c6a --- /dev/null +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--update_team-update_team_basic.sql @@ -0,0 +1,7 @@ +UPDATE "grafana"."team" +SET name = 'Team 1', + updated = '2023-01-01 12:00:00', + email = 'team1@example.com', + is_provisioned = TRUE, + external_uid = 'team-1-uid' +WHERE uid = 'team-1' diff --git a/pkg/registry/apis/iam/legacy/update_team.sql b/pkg/registry/apis/iam/legacy/update_team.sql new file mode 100644 index 00000000000..4aa7bb52d7a --- /dev/null +++ b/pkg/registry/apis/iam/legacy/update_team.sql @@ -0,0 +1,7 @@ +UPDATE {{ .Ident .TeamTable }} +SET name = {{ .Arg .Command.Name }}, + updated = {{ .Arg .Command.Updated }}, + email = {{ .Arg .Command.Email }}, + is_provisioned = {{ .Arg .Command.IsProvisioned }}, + external_uid = {{ .Arg .Command.ExternalUID }} +WHERE uid = {{ .Arg .Command.UID }} diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index c010698350d..767ec450bae 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -2,6 +2,7 @@ package iam import ( "context" + "fmt" "maps" "strings" @@ -353,6 +354,12 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm switch typedObj := a.GetObject().(type) { case *iamv0.ResourcePermission: return resourcepermission.ValidateCreateAndUpdateInput(ctx, typedObj) + case *iamv0.Team: + oldTeamObj, ok := a.GetOldObject().(*iamv0.Team) + if !ok { + return fmt.Errorf("expected old object to be a Team, got %T", oldTeamObj) + } + return team.ValidateOnUpdate(ctx, typedObj, oldTeamObj) } return nil case admission.Delete: diff --git a/pkg/registry/apis/iam/team/store.go b/pkg/registry/apis/iam/team/store.go index 88bf50986d6..e048fdb7a6d 100644 --- a/pkg/registry/apis/iam/team/store.go +++ b/pkg/registry/apis/iam/team/store.go @@ -112,7 +112,52 @@ func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation // Update implements rest.Updater. func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "update") + if !s.enableAuthnMutation { + return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "update") + } + + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + + oldObj, err := s.Get(ctx, name, nil) + if err != nil { + return oldObj, false, err + } + + obj, err := objInfo.UpdatedObject(ctx, oldObj) + if err != nil { + return oldObj, false, err + } + + teamObj, ok := obj.(*iamv0alpha1.Team) + if !ok { + return nil, false, fmt.Errorf("expected Team object, got %T", obj) + } + + if updateValidation != nil { + if err := updateValidation(ctx, obj, oldObj); err != nil { + return oldObj, false, err + } + } + + updateCmd := legacy.UpdateTeamCommand{ + UID: teamObj.Name, + Name: teamObj.Spec.Title, + Email: teamObj.Spec.Email, + IsProvisioned: teamObj.Spec.Provisioned, + ExternalUID: teamObj.Spec.ExternalUID, + } + + result, err := s.store.UpdateTeam(ctx, ns, updateCmd) + if err != nil { + return oldObj, false, err + } + + iamTeam := toTeamObject(result.Team, ns) + + return &iamTeam, false, nil } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { diff --git a/pkg/registry/apis/iam/team/validate.go b/pkg/registry/apis/iam/team/validate.go index 9205a560019..97777cb3c4e 100644 --- a/pkg/registry/apis/iam/team/validate.go +++ b/pkg/registry/apis/iam/team/validate.go @@ -30,3 +30,28 @@ func ValidateOnCreate(ctx context.Context, obj *iamv0alpha1.Team) error { return nil } + +func ValidateOnUpdate(ctx context.Context, obj, old *iamv0alpha1.Team) error { + requester, err := identity.GetRequester(ctx) + if err != nil { + return apierrors.NewUnauthorized("no identity found") + } + + if obj.Spec.Title == "" { + return apierrors.NewBadRequest("the team must have a title") + } + + if !requester.IsIdentityType(types.TypeServiceAccount) && obj.Spec.Provisioned && !old.Spec.Provisioned { + return apierrors.NewBadRequest("provisioned teams are only allowed for service accounts") + } + + if old.Spec.Provisioned && !obj.Spec.Provisioned { + return apierrors.NewBadRequest("provisioned teams cannot be updated to non-provisioned teams") + } + + if !obj.Spec.Provisioned && obj.Spec.ExternalUID != "" { + return apierrors.NewBadRequest("externalUID is only allowed for provisioned teams") + } + + return nil +} diff --git a/pkg/registry/apis/iam/team/validate_test.go b/pkg/registry/apis/iam/team/validate_test.go index d7acbab4915..1093a01f57d 100644 --- a/pkg/registry/apis/iam/team/validate_test.go +++ b/pkg/registry/apis/iam/team/validate_test.go @@ -115,3 +115,213 @@ func TestValidateOnCreate(t *testing.T) { }) } } + +func TestValidateOnUpdate(t *testing.T) { + tests := []struct { + name string + requester *identity.StaticRequester + obj *iamv0alpha1.Team + old *iamv0alpha1.Team + want error + }{ + { + name: "valid update - no changes to provisioned status", + requester: &identity.StaticRequester{ + Type: types.TypeUser, + OrgRole: identity.RoleAdmin, + }, + obj: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "updated title", + Email: "updated@test.com", + }, + }, + old: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "original title", + Email: "original@test.com", + }, + }, + want: nil, + }, + { + name: "valid update - service account changing to provisioned", + requester: &identity.StaticRequester{ + Type: types.TypeServiceAccount, + OrgRole: identity.RoleAdmin, + }, + obj: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "updated title", + Email: "updated@test.com", + Provisioned: true, + ExternalUID: "test-uid", + }, + }, + old: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "original title", + Email: "original@test.com", + }, + }, + want: nil, + }, + { + name: "valid update - already provisioned team", + requester: &identity.StaticRequester{ + Type: types.TypeServiceAccount, + OrgRole: identity.RoleAdmin, + }, + obj: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "updated title", + Email: "updated@test.com", + Provisioned: true, + ExternalUID: "updated-uid", + }, + }, + old: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "original title", + Email: "original@test.com", + Provisioned: true, + ExternalUID: "original-uid", + }, + }, + want: nil, + }, + { + name: "invalid update - no title", + requester: &identity.StaticRequester{ + Type: types.TypeUser, + OrgRole: identity.RoleAdmin, + }, + obj: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "", + Email: "updated@test.com", + }, + }, + old: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "original title", + Email: "original@test.com", + }, + }, + want: apierrors.NewBadRequest("the team must have a title"), + }, + { + name: "invalid update - user trying to change to provisioned", + requester: &identity.StaticRequester{ + Type: types.TypeUser, + OrgRole: identity.RoleAdmin, + }, + obj: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "updated title", + Email: "updated@test.com", + Provisioned: true, + ExternalUID: "test-uid", + }, + }, + old: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "original title", + Email: "original@test.com", + }, + }, + want: apierrors.NewBadRequest("provisioned teams are only allowed for service accounts"), + }, + { + name: "invalid update - changing from provisioned to non-provisioned", + requester: &identity.StaticRequester{ + Type: types.TypeServiceAccount, + OrgRole: identity.RoleAdmin, + }, + obj: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "updated title", + Email: "updated@test.com", + }, + }, + old: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "original title", + Email: "original@test.com", + Provisioned: true, + ExternalUID: "original-uid", + }, + }, + want: apierrors.NewBadRequest("provisioned teams cannot be updated to non-provisioned teams"), + }, + { + name: "invalid update - has externalUID but not provisioned", + requester: &identity.StaticRequester{ + Type: types.TypeUser, + OrgRole: identity.RoleAdmin, + }, + obj: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "updated title", + Email: "updated@test.com", + ExternalUID: "test-uid", + }, + }, + old: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "original title", + Email: "original@test.com", + }, + }, + want: apierrors.NewBadRequest("externalUID is only allowed for provisioned teams"), + }, + { + name: "invalid update - no requester in context", + requester: nil, + obj: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "updated title", + Email: "updated@test.com", + }, + }, + old: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "original title", + Email: "original@test.com", + }, + }, + want: apierrors.NewUnauthorized("no identity found"), + }, + { + name: "valid update - adding externalUID to provisioned team", + requester: &identity.StaticRequester{ + Type: types.TypeServiceAccount, + OrgRole: identity.RoleAdmin, + }, + obj: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "updated title", + Email: "updated@test.com", + Provisioned: true, + ExternalUID: "new-uid", + }, + }, + old: &iamv0alpha1.Team{ + Spec: iamv0alpha1.TeamSpec{ + Title: "original title", + Email: "original@test.com", + Provisioned: true, + }, + }, + want: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := identity.WithRequester(context.Background(), test.requester) + err := ValidateOnUpdate(ctx, test.obj, test.old) + assert.Equal(t, test.want, err) + }) + } +} diff --git a/pkg/tests/apis/iam/team_integration_test.go b/pkg/tests/apis/iam/team_integration_test.go index f6e0a4605f6..e8bb115dfb7 100644 --- a/pkg/tests/apis/iam/team_integration_test.go +++ b/pkg/tests/apis/iam/team_integration_test.go @@ -38,17 +38,18 @@ func TestIntegrationTeams(t *testing.T) { featuremgmt.FlagKubernetesAuthnMutation, }, }) + doTeamCRUDTestsUsingTheNewAPIs(t, helper) if mode < 3 { - doTeamCRUDTestsUsingTheLegacyAPIs(t, helper) + doTeamCRUDTestsUsingTheLegacyAPIs(t, helper, mode) } }) } } func doTeamCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) { - t.Run("should create/get/delete team using the new APIs as a GrafanaAdmin", func(t *testing.T) { + t.Run("should create/get/update/delete team using the new APIs as a GrafanaAdmin", func(t *testing.T) { ctx := context.Background() teamClient := helper.GetResourceClient(apis.ResourceClientArgs{ @@ -57,6 +58,7 @@ func doTeamCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) { GVR: gvrTeams, }) + // Create the team created, err := teamClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/team-test-create-v0.yaml"), metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, created) @@ -69,6 +71,7 @@ func doTeamCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) { createdUID := created.GetName() require.NotEmpty(t, createdUID) + // Get the team fetched, err := teamClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) require.NoError(t, err) require.NotNil(t, fetched) @@ -81,6 +84,26 @@ func doTeamCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) { require.Equal(t, createdUID, fetched.GetName()) require.Equal(t, "default", fetched.GetNamespace()) + // Update the team + updatedTeam, err := teamClient.Resource.Update(ctx, helper.LoadYAMLOrJSONFile("testdata/team-test-update-v0.yaml"), metav1.UpdateOptions{}) + require.NoError(t, err) + require.NotNil(t, updatedTeam) + + updatedSpec := updatedTeam.Object["spec"].(map[string]interface{}) + require.Equal(t, "Test Team 2", updatedSpec["title"]) + require.Equal(t, "testteam2@example123.com", updatedSpec["email"]) + require.Equal(t, false, updatedSpec["provisioned"]) + + verifiedTeam, err := teamClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, verifiedTeam) + + verifiedSpec := verifiedTeam.Object["spec"].(map[string]interface{}) + require.Equal(t, "Test Team 2", verifiedSpec["title"]) + require.Equal(t, "testteam2@example123.com", verifiedSpec["email"]) + require.Equal(t, false, verifiedSpec["provisioned"]) + + // Delete the team err = teamClient.Resource.Delete(ctx, createdUID, metav1.DeleteOptions{}) require.NoError(t, err) @@ -202,12 +225,14 @@ func doTeamCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) { }) } -func doTeamCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper) { - t.Run("should create team using legacy APIs and get/delete it using the new APIs", func(t *testing.T) { +func doTeamCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper, mode rest.DualWriterMode) { + t.Run("should create team using legacy APIs and get/update/delete it using the new APIs", func(t *testing.T) { ctx := context.Background() + teamClient := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - GVR: gvrTeams, + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + GVR: gvrTeams, }) legacyTeamPayload := `{ @@ -243,6 +268,31 @@ func doTeamCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper) require.Equal(t, rsp.Result.UID, team.GetName()) require.Equal(t, "default", team.GetNamespace()) + // Updating the team is not supported in Mode2 if the team has been created using the legacy APIs + if mode < rest.Mode2 { + team.Object["spec"].(map[string]interface{})["title"] = "Updated Test Team 2" + team.Object["spec"].(map[string]interface{})["email"] = "updated@example.com" + + updatedTeam, err := teamClient.Resource.Update(ctx, team, metav1.UpdateOptions{}) + require.NoError(t, err) + require.NotNil(t, updatedTeam) + + updatedSpec := updatedTeam.Object["spec"].(map[string]interface{}) + require.Equal(t, "Updated Test Team 2", updatedSpec["title"]) + require.Equal(t, "updated@example.com", updatedSpec["email"]) + require.Equal(t, false, updatedSpec["provisioned"]) + + verifiedTeam, err := teamClient.Resource.Get(ctx, rsp.Result.UID, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, verifiedTeam) + + verifiedSpec := verifiedTeam.Object["spec"].(map[string]interface{}) + require.Equal(t, "Updated Test Team 2", verifiedSpec["title"]) + require.Equal(t, "updated@example.com", verifiedSpec["email"]) + require.Equal(t, false, verifiedSpec["provisioned"]) + } + + // Delete the team err = teamClient.Resource.Delete(ctx, rsp.Result.UID, metav1.DeleteOptions{}) require.NoError(t, err) diff --git a/pkg/tests/apis/iam/testdata/team-test-update-v0.yaml b/pkg/tests/apis/iam/testdata/team-test-update-v0.yaml new file mode 100644 index 00000000000..0b01271cb3f --- /dev/null +++ b/pkg/tests/apis/iam/testdata/team-test-update-v0.yaml @@ -0,0 +1,7 @@ +apiVersion: iam.grafana.app/v0alpha1 +kind: Team +metadata: + name: test-team-1 +spec: + title: "Test Team 2" + email: testteam2@example123.com From ed482219c60099b6c4435d0679f17b6f984355c4 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Fri, 3 Oct 2025 03:52:13 -0600 Subject: [PATCH 14/14] Provisioning: Specify managed by git sync (#111969) * Provisioning: Specify managed by git sync * linter * Update public/app/features/provisioning/Wizard/hooks/useResourceStats.ts Co-authored-by: Alex Khomenko * Update public/app/features/provisioning/Shared/RepositoryList.tsx Co-authored-by: Alex Khomenko * Update public/app/features/provisioning/Wizard/hooks/useResourceStats.ts Co-authored-by: Alex Khomenko * Update public/app/features/provisioning/Shared/RepositoryList.tsx Co-authored-by: Alex Khomenko * fix frontend lint --------- Co-authored-by: Alex Khomenko Co-authored-by: Ryan McKinley --- .../provisioning/Shared/RepositoryList.tsx | 4 +- .../Wizard/hooks/useResourceStats.ts | 41 +++++++++++++++---- public/locales/en-US/grafana.json | 6 +-- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/public/app/features/provisioning/Shared/RepositoryList.tsx b/public/app/features/provisioning/Shared/RepositoryList.tsx index be7ff34c074..c84a3e7e6dd 100644 --- a/public/app/features/provisioning/Shared/RepositoryList.tsx +++ b/public/app/features/provisioning/Shared/RepositoryList.tsx @@ -43,13 +43,13 @@ export function RepositoryList({ items }: Props) { i18nKey="provisioning.folder-repository-list.partial-managed" values={{ managedCount, resourceCount }} > - {{ managedCount }}/{{ resourceCount }} resources managed. + {{ managedCount }}/{{ resourceCount }} resources managed by Git sync.
{unmanagedCount > 0 && ( <> {' '} - {{ count: unmanagedCount }} resources aren't managed as code yet. + {{ count: unmanagedCount }} resources aren't managed by Git sync. )} diff --git a/public/app/features/provisioning/Wizard/hooks/useResourceStats.ts b/public/app/features/provisioning/Wizard/hooks/useResourceStats.ts index 4afc252e0e5..49573b4e793 100644 --- a/public/app/features/provisioning/Wizard/hooks/useResourceStats.ts +++ b/public/app/features/provisioning/Wizard/hooks/useResourceStats.ts @@ -11,24 +11,27 @@ import { useGetRepositoryFilesQuery, useGetResourceStatsQuery, } from 'app/api/clients/provisioning/v0alpha1'; +import { ManagerKind } from 'app/features/apiserver/types'; function getManagedCount(managed?: ManagerStats[]) { let totalCount = 0; // Loop through each managed repository managed?.forEach((manager) => { - // Loop through stats inside each manager and sum up the counts - manager.stats.forEach((stat) => { - if (stat.group === 'folder.grafana.app' || stat.group === 'dashboard.grafana.app') { - totalCount += stat.count; - } - }); + if (manager.kind === ManagerKind.Repo) { + // Loop through stats inside each manager and sum up the counts + manager.stats.forEach((stat) => { + if (stat.group === 'folder.grafana.app' || stat.group === 'dashboard.grafana.app') { + totalCount += stat.count; + } + }); + } }); return totalCount; } -function getResourceCount(stats?: ResourceCount[]) { +function getResourceCount(stats?: ResourceCount[], managed?: ManagerStats[]) { let counts: string[] = []; let resourceCount = 0; @@ -46,6 +49,26 @@ function getResourceCount(stats?: ResourceCount[]) { } }); + managed?.forEach((manager) => { + if (manager.kind !== ManagerKind.Repo) { + manager.stats.forEach((stat) => { + switch (stat.group) { + case 'folders': + case 'folder.grafana.app': + resourceCount += stat.count; + counts.push(t('provisioning.bootstrap-step.folders-count', '{{count}} folder', { count: stat.count })); + break; + case 'dashboard.grafana.app': + resourceCount += stat.count; + counts.push( + t('provisioning.bootstrap-step.dashboards-count', '{{count}} dashboard', { count: stat.count }) + ); + break; + } + }); + } + }); + return { counts, resourceCount, @@ -92,7 +115,9 @@ export function useResourceStats(repoName?: string, isLegacyStorage?: boolean, s return { // managed does not exist in response when first time connecting to a repo managedCount: getManagedCount(resourceStatsQuery.data?.managed), - unmanagedCount: getResourceCount(resourceStatsQuery.data?.unmanaged).resourceCount, + // "unmanaged" means unmanaged by git sync. it may still be managed by other means, like terraform, plugins, file provisioning, etc. + unmanagedCount: getResourceCount(resourceStatsQuery.data?.unmanaged, resourceStatsQuery.data?.managed) + .resourceCount, }; }, [resourceStatsQuery.data]); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index d86e265b519..38084055225 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11353,10 +11353,10 @@ "all-resources-managed_one": "All {{count}} resource is managed", "all-resources-managed_other": "All {{count}} resources are managed", "no-results-matching-your-query": "No results matching your query", - "partial-managed": "{{managedCount}}/{{resourceCount}} resources managed.", + "partial-managed": "{{managedCount}}/{{resourceCount}} resources managed by Git sync.", "placeholder-search": "Search", - "unmanaged-resources_one": "{{count}} resource isn't managed as code yet.", - "unmanaged-resources_other": "{{count}} resources aren't managed as code yet." + "unmanaged-resources_one": "{{count}} resource isn't managed by git sync.", + "unmanaged-resources_other": "{{count}} resources aren't managed by git sync." }, "get-default-values": { "title": {