Merge remote-tracking branch 'origin/main' into query-history-app

This commit is contained in:
Ryan McKinley
2025-10-03 12:57:09 +03:00
118 changed files with 1582 additions and 2138 deletions
+1
View File
@@ -6,4 +6,5 @@ generate: install-app-sdk update-app-sdk
--source=./kinds/ \
--gogenpath=./pkg/apis \
--grouping=group \
--genoperatorstate=false \
--defencoding=none
@@ -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)
}
@@ -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)
}
@@ -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"
)
@@ -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)
)
@@ -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;
}
@@ -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<string, any>;
}
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<string, OperatorState>;
// additionalFields is reserved for future use
additionalFields?: Record<string, any>;
}
export const defaultStatus = (): Status => ({
});
+1
View File
@@ -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
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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"},
}
}
+1
View File
@@ -6,4 +6,5 @@ generate: install-app-sdk update-app-sdk
--source=./kinds/ \
--gogenpath=./pkg/apis \
--grouping=group \
--genoperatorstate=false \
--defencoding=none
@@ -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)
}
@@ -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)
}
@@ -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"
)
@@ -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)
}
@@ -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)
}
@@ -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"
)
@@ -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"},
},
},
}
}
@@ -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)
)
+7 -2
View File
@@ -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
@@ -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
}
@@ -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)
})
}
}
@@ -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
}
@@ -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)
})
}
}
@@ -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 >}}
@@ -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.
@@ -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`
@@ -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 |
+18 -9
View File
@@ -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()
+23 -2
View File
@@ -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
}
+50 -9
View File
@@ -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
}
+17
View File
@@ -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
+1
View File
@@ -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)
+19
View File
@@ -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",
+85
View File
@@ -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
}
@@ -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'
@@ -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'
@@ -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'
@@ -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 }}
+7
View File
@@ -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:
+46 -1
View File
@@ -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) {
+25
View File
@@ -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
}
+210
View File
@@ -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)
})
}
}
@@ -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)
@@ -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)
+11 -32
View File
@@ -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 {
@@ -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) {
+4 -5
View File
@@ -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
@@ -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},
}
}
@@ -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
}
+19 -6
View File
@@ -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
}
@@ -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()
+4 -10
View File
@@ -548,10 +548,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)
@@ -815,7 +812,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
}
@@ -1157,10 +1154,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)
@@ -1426,7 +1420,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
}
+2 -7
View File
@@ -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) {
@@ -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
}
@@ -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"}
@@ -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)
}
@@ -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)
}
@@ -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()
}
@@ -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)
@@ -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{
+72 -71
View File
@@ -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 {
@@ -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) {
+1 -2
View File
@@ -69,8 +69,7 @@ func TestIntegrationIdentity(t *testing.T) {
"title": "staff",
"provisioned": false,
"externalUID": ""
},
"status": {}
}
}
]
}`, found)
+56 -6
View File
@@ -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)
+7
View File
@@ -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
@@ -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": {}
}
}
},
@@ -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",
+1 -19
View File
@@ -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';
@@ -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 */
+14
View File
@@ -15,12 +15,26 @@ interface CreateBaseQueryOptions {
export function createBaseQuery({ baseURL }: CreateBaseQueryOptions): BaseQueryFn<RequestOptions> {
async function backendSrvBaseQuery(requestOptions: RequestOptions) {
try {
const headers: Record<string, string> = {
...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 };
+3 -1
View File
@@ -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;
}
@@ -206,7 +206,13 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
searchStateManager={stateManager}
/>
) : (
<BrowseView permissions={permissions} width={width} height={height} folderUID={folderUID} />
<BrowseView
permissions={permissions}
width={width}
height={height}
folderUID={folderUID}
isReadOnlyRepo={isReadOnlyRepo}
/>
)
}
</AutoSizer>
@@ -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}
>
<Trans i18nKey="browse-dashboards.empty-state.button-title">Create dashboard</Trans>
</LinkButton>
@@ -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 && (
<Trans i18nKey="browse-dashboards.empty-state.pro-tip">
Add/move dashboards to your folder at{' '}
<TextLink external={false} href="/dashboards">
+1 -1
View File
@@ -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),
+27 -22
View File
@@ -799,28 +799,30 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
onClickHideField={hideField}
/>
)}
<PanelChrome
title={t('explore.unthemed-logs.title-logs-volume', 'Logs volume')}
collapsible
collapsed={!logsVolumeEnabled}
onToggleCollapse={onToggleLogsVolumeCollapse}
>
{logsVolumeEnabled && (
<LogsVolumePanelList
toggleLegendRef={toggleLegendRef}
absoluteRange={absoluteRange}
width={width}
logsVolumeData={logsVolumeData}
onUpdateTimeRange={onChangeTime}
timeZone={timeZone}
splitOpen={splitOpen}
onLoadLogsVolume={loadLogsVolumeData}
onDisplayedSeriesChanged={onDisplayedSeriesChanged}
eventBus={logsVolumeEventBus}
onClose={() => onToggleLogsVolumeCollapse(true)}
/>
)}
</PanelChrome>
<div className={styles.logsVolumePanel}>
<PanelChrome
title={t('explore.unthemed-logs.title-logs-volume', 'Logs volume')}
collapsible
collapsed={!logsVolumeEnabled}
onToggleCollapse={onToggleLogsVolumeCollapse}
>
{logsVolumeEnabled && (
<LogsVolumePanelList
toggleLegendRef={toggleLegendRef}
absoluteRange={absoluteRange}
width={width}
logsVolumeData={logsVolumeData}
onUpdateTimeRange={onChangeTime}
timeZone={timeZone}
splitOpen={splitOpen}
onLoadLogsVolume={loadLogsVolumeData}
onDisplayedSeriesChanged={onDisplayedSeriesChanged}
eventBus={logsVolumeEventBus}
onClose={() => onToggleLogsVolumeCollapse(true)}
/>
)}
</PanelChrome>
</div>
<PanelChrome
titleItems={[
config.featureToggles.logsExploreTableVisualisation ? (
@@ -1278,6 +1280,9 @@ const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: n
overflow: 'visible',
...(config.featureToggles.logsInfiniteScrolling && { marginBottom: '0px' }),
}),
logsVolumePanel: css({
marginBottom: theme.spacing(1.5),
}),
};
};
@@ -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.
</Trans>
{unmanagedCount > 0 && (
<>
{' '}
<Trans i18nKey="provisioning.folder-repository-list.unmanaged-resources" count={unmanagedCount}>
{{ count: unmanagedCount }} resources aren&apos;t managed as code yet.
{{ count: unmanagedCount }} resources aren&apos;t managed by Git sync.
</Trans>
</>
)}
@@ -12,7 +12,12 @@ import { getGitProviderFields } from './fields';
import { WizardFormData } from './types';
export const FinishStep = memo(function FinishStep() {
const { register, watch, setValue } = useFormContext<WizardFormData>();
const {
register,
watch,
setValue,
formState: { errors },
} = useFormContext<WizardFormData>();
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}
>
<Input
{...register('repository.sync.intervalSeconds', {
@@ -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]);
@@ -3,7 +3,7 @@ import { ErrorDetails } from 'app/api/clients/provisioning/v0alpha1';
import { WizardFormData } from '../Wizard/types';
export type RepositoryField = keyof WizardFormData['repository'];
export type RepositoryFormPath = `repository.${RepositoryField}`;
export type RepositoryFormPath = `repository.${RepositoryField}` | `repository.sync.intervalSeconds`;
export type FormErrorTuple = [RepositoryFormPath | null, { message: string } | null];
/**
@@ -25,7 +25,13 @@ export const getFormErrors = (errors: ErrorDetails[]): FormErrorTuple => {
'bitbucket.url',
'git.branch',
'git.url',
'sync.intervalSeconds',
];
const nestedFieldMap: Record<string, RepositoryFormPath> = {
'sync.intervalSeconds': 'repository.sync.intervalSeconds',
};
const fieldMap: Record<string, RepositoryFormPath> = {
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];
+1
View File
@@ -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.",

Some files were not shown because too many files have changed in this diff Show More