diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 30bba5db0ff..d8a1e4104bf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -85,6 +85,7 @@ # Git Sync frontend owned by frontend team as a whole. /apps/alerting/ @grafana/alerting-backend +/apps/quotas/ @grafana/grafana-search-and-storage /apps/dashboard/ @grafana/grafana-app-platform-squad @grafana/dashboards-squad /apps/folder/ @grafana/grafana-app-platform-squad /apps/playlist/ @grafana/grafana-app-platform-squad diff --git a/.github/commands.json b/.github/commands.json index 29106576726..0db8a607b53 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -1226,5 +1226,13 @@ "addToProject": { "url": "https://github.com/orgs/grafana/projects/69" } + }, + { + "type": "label", + "name": "area/suggestions", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/56" + } } ] diff --git a/.github/pr-commands.json b/.github/pr-commands.json index 8b4472fe02a..ab42e204fce 100644 --- a/.github/pr-commands.json +++ b/.github/pr-commands.json @@ -469,5 +469,15 @@ "addToProject": { "url": "https://github.com/orgs/grafana/projects/190" } + }, + { + "type": "changedfiles", + "matches": [ + "public/app/features/panel/suggestions/**/*", + "public/app/plugins/panel/**/suggestions.ts", + "packages/grafana-data/src/types/suggestions*" + ], + "action": "updateLabel", + "addLabel": "area/suggestions" } ] diff --git a/.github/workflows/auto-triager/labels.txt b/.github/workflows/auto-triager/labels.txt index 18ef2df93d8..451719ee0f7 100644 --- a/.github/workflows/auto-triager/labels.txt +++ b/.github/workflows/auto-triager/labels.txt @@ -85,6 +85,7 @@ area/scenes area/search area/security area/streaming +area/suggestions area/templating/repeating area/tooltip area/transformations diff --git a/Dockerfile b/Dockerfile index d3f63dd0544..2b16926a836 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ ARG JS_SRC=js-builder # Dependabot cannot update dependencies listed in ARGs # By using FROM instructions we can delegate dependency updates to dependabot -FROM alpine:3.22.2 AS alpine-base +FROM alpine:3.23.0 AS alpine-base FROM ubuntu:22.04 AS ubuntu-base FROM golang:1.25.5-alpine AS go-builder-base FROM --platform=${JS_PLATFORM} node:24-alpine AS js-builder-base @@ -93,6 +93,7 @@ COPY pkg/storage/unified/apistore pkg/storage/unified/apistore COPY pkg/semconv pkg/semconv COPY pkg/aggregator pkg/aggregator COPY apps/playlist apps/playlist +COPY apps/quotas apps/quotas COPY apps/plugins apps/plugins COPY apps/shorturl apps/shorturl COPY apps/annotation apps/annotation diff --git a/apps/alerting/historian/go.sum b/apps/alerting/historian/go.sum index b4d2d1dc2e2..9c00f19a029 100644 --- a/apps/alerting/historian/go.sum +++ b/apps/alerting/historian/go.sum @@ -224,6 +224,8 @@ github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba h1:psKWNETD5nGxmF github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4= github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLmAhwA4m6iGUD4w1YkydEWWjazn9qxCFT8W0= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index 0a19120fe80..2182e0af949 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -57,6 +57,7 @@ require ( github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.7.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/jaegertracing/jaeger-idl v0.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 0faeaaf78ba..a93f97a1d0e 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -112,6 +112,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/jaegertracing/jaeger-idl v0.5.0 h1:zFXR5NL3Utu7MhPg8ZorxtCBjHrL3ReM1VoB65FOFGE= diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 3fe93d7305f..c13eb866c80 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -768,6 +768,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged" // Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing). VariableHide: *"dontHide" | "hideLabel" | "hideVariable" +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +VariableRegexApplyTo: *"value" | "text" + // Determine the origin of the adhoc variable filter FilterOrigin: "dashboard" @@ -803,6 +807,7 @@ QueryVariableSpec: { datasource?: DataSourceRef query: DataQueryKind regex: string | *"" + regexApplyTo?: VariableRegexApplyTo sort: VariableSort definition?: string options: [...VariableOption] | *[] diff --git a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue index ef4a27fd6b7..bb833795354 100644 --- a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue @@ -772,6 +772,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged" // Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing), `inControlsMenu` (show in a drop-down menu). VariableHide: *"dontHide" | "hideLabel" | "hideVariable" | "inControlsMenu" +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +VariableRegexApplyTo: *"value" | "text" + // Determine the origin of the adhoc variable filter FilterOrigin: "dashboard" @@ -806,6 +810,7 @@ QueryVariableSpec: { description?: string query: DataQueryKind regex: string | *"" + regexApplyTo?: VariableRegexApplyTo sort: VariableSort definition?: string options: [...VariableOption] | *[] diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue index d2a65bbbf24..4a255a57ba6 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue @@ -222,6 +222,8 @@ lineage: schemas: [{ // Optional field, if you want to extract part of a series name or metric node segment. // Named capture groups can be used to separate the display text and value. regex?: string + // Determine whether regex applies to variable value or display text + regexApplyTo?: #VariableRegexApplyTo // Additional static options for query variable staticOptions?: [...#VariableOption] // Ordering of static options in relation to options returned from data source for query variable @@ -249,6 +251,10 @@ lineage: schemas: [{ // Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing), 3 (show under the controls dropdown menu). #VariableHide: 0 | 1 | 2 | 3 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable|inControlsMenu") @grafana(TSVeneer="type") + // Determine whether regex applies to variable value or display text + // Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users) + #VariableRegexApplyTo: "value" | "text" @cuetsy(kind="type") + // Sort variable options // Accepted values are: // `0`: No sorting diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue index d2a65bbbf24..4a255a57ba6 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/dashboard_kind.cue @@ -222,6 +222,8 @@ lineage: schemas: [{ // Optional field, if you want to extract part of a series name or metric node segment. // Named capture groups can be used to separate the display text and value. regex?: string + // Determine whether regex applies to variable value or display text + regexApplyTo?: #VariableRegexApplyTo // Additional static options for query variable staticOptions?: [...#VariableOption] // Ordering of static options in relation to options returned from data source for query variable @@ -249,6 +251,10 @@ lineage: schemas: [{ // Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing), 3 (show under the controls dropdown menu). #VariableHide: 0 | 1 | 2 | 3 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable|inControlsMenu") @grafana(TSVeneer="type") + // Determine whether regex applies to variable value or display text + // Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users) + #VariableRegexApplyTo: "value" | "text" @cuetsy(kind="type") + // Sort variable options // Accepted values are: // `0`: No sorting diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index d822ad9f38a..ec8d7eead87 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -772,6 +772,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged" // Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing). VariableHide: *"dontHide" | "hideLabel" | "hideVariable" +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +VariableRegexApplyTo: *"value" | "text" + // Determine the origin of the adhoc variable filter FilterOrigin: "dashboard" @@ -807,6 +811,7 @@ QueryVariableSpec: { datasource?: DataSourceRef query: DataQueryKind regex: string | *"" + regexApplyTo?: VariableRegexApplyTo sort: VariableSort definition?: string options: [...VariableOption] | *[] diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index 625c6fe17c0..883c5399663 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -1364,6 +1364,7 @@ type DashboardQueryVariableSpec struct { Datasource *DashboardDataSourceRef `json:"datasource,omitempty"` Query DashboardDataQueryKind `json:"query"` Regex string `json:"regex"` + RegexApplyTo *DashboardVariableRegexApplyTo `json:"regexApplyTo,omitempty"` Sort DashboardVariableSort `json:"sort"` Definition *string `json:"definition,omitempty"` Options []DashboardVariableOption `json:"options"` @@ -1393,6 +1394,7 @@ func NewDashboardQueryVariableSpec() *DashboardQueryVariableSpec { SkipUrlSync: false, Query: *NewDashboardDataQueryKind(), Regex: "", + RegexApplyTo: (func(input DashboardVariableRegexApplyTo) *DashboardVariableRegexApplyTo { return &input })(DashboardVariableRegexApplyToValue), Options: []DashboardVariableOption{}, Multi: false, IncludeAll: false, @@ -1443,6 +1445,16 @@ const ( DashboardVariableRefreshOnTimeRangeChanged DashboardVariableRefresh = "onTimeRangeChanged" ) +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +// +k8s:openapi-gen=true +type DashboardVariableRegexApplyTo string + +const ( + DashboardVariableRegexApplyToValue DashboardVariableRegexApplyTo = "value" + DashboardVariableRegexApplyToText DashboardVariableRegexApplyTo = "text" +) + // Sort variable options // Accepted values are: // `disabled`: No sorting diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index ff7429d7c2b..d697aa4f8b9 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -3646,6 +3646,12 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableSpec(ref common.Re Format: "", }, }, + "regexApplyTo": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, "sort": { SchemaProps: spec.SchemaProps{ Default: "", diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue index 0c061075e6d..12d7bec351b 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue @@ -776,6 +776,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged" // Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing), `inControlsMenu` (show in a drop-down menu). VariableHide: *"dontHide" | "hideLabel" | "hideVariable" | "inControlsMenu" +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +VariableRegexApplyTo: *"value" | "text" + // Determine the origin of the adhoc variable filter FilterOrigin: "dashboard" @@ -810,6 +814,7 @@ QueryVariableSpec: { description?: string query: DataQueryKind regex: string | *"" + regexApplyTo?: VariableRegexApplyTo sort: VariableSort definition?: string options: [...VariableOption] | *[] diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go index a6e63aa0bcc..a8ec1537e38 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go @@ -1367,6 +1367,7 @@ type DashboardQueryVariableSpec struct { Description *string `json:"description,omitempty"` Query DashboardDataQueryKind `json:"query"` Regex string `json:"regex"` + RegexApplyTo *DashboardVariableRegexApplyTo `json:"regexApplyTo,omitempty"` Sort DashboardVariableSort `json:"sort"` Definition *string `json:"definition,omitempty"` Options []DashboardVariableOption `json:"options"` @@ -1396,6 +1397,7 @@ func NewDashboardQueryVariableSpec() *DashboardQueryVariableSpec { SkipUrlSync: false, Query: *NewDashboardDataQueryKind(), Regex: "", + RegexApplyTo: (func(input DashboardVariableRegexApplyTo) *DashboardVariableRegexApplyTo { return &input })(DashboardVariableRegexApplyToValue), Options: []DashboardVariableOption{}, Multi: false, IncludeAll: false, @@ -1447,6 +1449,16 @@ const ( DashboardVariableRefreshOnTimeRangeChanged DashboardVariableRefresh = "onTimeRangeChanged" ) +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +// +k8s:openapi-gen=true +type DashboardVariableRegexApplyTo string + +const ( + DashboardVariableRegexApplyToValue DashboardVariableRegexApplyTo = "value" + DashboardVariableRegexApplyToText DashboardVariableRegexApplyTo = "text" +) + // Sort variable options // Accepted values are: // `disabled`: No sorting diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go index 3a129374192..2b1fe573336 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go @@ -3656,6 +3656,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardQueryVariableSpec(ref common.Ref Format: "", }, }, + "regexApplyTo": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, "sort": { SchemaProps: spec.SchemaProps{ Default: "", diff --git a/apps/dashboard/pkg/apis/dashboard_manifest.go b/apps/dashboard/pkg/apis/dashboard_manifest.go index c4e35bd8f40..c815e815e08 100644 --- a/apps/dashboard/pkg/apis/dashboard_manifest.go +++ b/apps/dashboard/pkg/apis/dashboard_manifest.go @@ -32,10 +32,10 @@ var ( rawSchemaDashboardv1beta1 = []byte(`{"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) versionSchemaDashboardv1beta1 app.VersionSchema _ = json.Unmarshal(rawSchemaDashboardv1beta1, &versionSchemaDashboardv1beta1) - rawSchemaDashboardv2alpha1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a DataQueryKind is the datasource type","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["kind","spec"],"type":"object"},"DataSourceRef":{"additionalProperties":false,"properties":{"type":{"description":"The plugin type-id","type":"string"},"uid":{"description":"Specific datasource instance","type":"string"}},"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"description":"Switch variable specification","properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing).","enum":["dontHide","hideLabel","hideVariable"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a VizConfigKind is the plugin ID","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"}},"required":["kind","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"pluginVersion":{"type":"string"}},"required":["pluginVersion","options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + rawSchemaDashboardv2alpha1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a DataQueryKind is the datasource type","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["kind","spec"],"type":"object"},"DataSourceRef":{"additionalProperties":false,"properties":{"type":{"description":"The plugin type-id","type":"string"},"uid":{"description":"Specific datasource instance","type":"string"}},"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"datasource":{"$ref":"#/components/schemas/DataSourceRef"},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"regexApplyTo":{"$ref":"#/components/schemas/VariableRegexApplyTo","default":"value"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"description":"Switch variable specification","properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing).","enum":["dontHide","hideLabel","hideVariable"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableRegexApplyTo":{"description":"Determine whether regex applies to variable value or display text\nAccepted values are ` + "`" + `value` + "`" + ` (apply to value used in queries) or ` + "`" + `text` + "`" + ` (apply to display text shown to users)","enum":["value","text"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a VizConfigKind is the plugin ID","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"}},"required":["kind","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"pluginVersion":{"type":"string"}},"required":["pluginVersion","options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) versionSchemaDashboardv2alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaDashboardv2alpha1, &versionSchemaDashboardv2alpha1) - rawSchemaDashboardv2beta1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQueryPlacement":{"const":"inControlsMenu","description":"Annotation Query placement. Defines where the annotation query should be displayed.\n- \"inControlsMenu\" renders the annotation query in the dashboard controls dropdown menu","type":"string"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties. Should not be available in as code tooling.","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"placement":{"$ref":"#/components/schemas/AnnotationQueryPlacement","description":"Placement can be used to display the annotation query somewhere else on the dashboard other than the default location."},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["query","enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"datasource":{"additionalProperties":false,"description":"New type for datasource reference\nNot creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS.","properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"DataQuery","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"version":{"default":"v0","type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"const":"onTimeRangeChanged","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"default":"A","type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeCompare":{"type":"string"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing), ` + "`" + `inControlsMenu` + "`" + ` (show in a drop-down menu).","enum":["dontHide","hideLabel","hideVariable","inControlsMenu"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"group":{"description":"The group is the plugin ID","type":"string"},"kind":{"const":"VizConfig","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"},"version":{"type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) + rawSchemaDashboardv2beta1 = []byte(`{"Action":{"additionalProperties":false,"properties":{"confirmation":{"type":"string"},"fetch":{"$ref":"#/components/schemas/FetchOptions"},"infinity":{"$ref":"#/components/schemas/InfinityOptions"},"oneClick":{"type":"boolean"},"style":{"additionalProperties":false,"properties":{"backgroundColor":{"type":"string"}},"type":"object"},"title":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionType"},"variables":{"items":{"$ref":"#/components/schemas/ActionVariable"},"type":"array"}},"required":["type","title"],"type":"object"},"ActionType":{"enum":["fetch","infinity"],"type":"string"},"ActionVariable":{"additionalProperties":false,"properties":{"key":{"type":"string"},"name":{"type":"string"},"type":{"$ref":"#/components/schemas/ActionVariableType"}},"required":["key","name","type"],"type":"object"},"ActionVariableType":{"const":"string","description":"Action variable type","type":"string"},"AdHocFilterWithLabels":{"additionalProperties":false,"description":"Define the AdHocFilterWithLabels type","properties":{"condition":{"description":"@deprecated","type":"string"},"forceEdit":{"type":"boolean"},"key":{"type":"string"},"keyLabel":{"type":"string"},"operator":{"type":"string"},"origin":{"$ref":"#/components/schemas/FilterOrigin"},"value":{"type":"string"},"valueLabels":{"items":{"type":"string"},"type":"array"},"values":{"items":{"type":"string"},"type":"array"}},"required":["key","operator","value"],"type":"object"},"AdhocVariableKind":{"additionalProperties":false,"description":"Adhoc variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"AdhocVariable","type":"string"},"spec":{"$ref":"#/components/schemas/AdhocVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"AdhocVariableSpec":{"additionalProperties":false,"description":"Adhoc variable specification","properties":{"allowCustomValue":{"default":true,"type":"boolean"},"baseFilters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"defaultKeys":{"items":{"$ref":"#/components/schemas/MetricFindValue"},"type":"array"},"description":{"type":"string"},"filters":{"items":{"$ref":"#/components/schemas/AdHocFilterWithLabels"},"type":"array"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","baseFilters","filters","defaultKeys","hide","skipUrlSync","allowCustomValue"],"type":"object"},"AnnotationEventFieldMapping":{"additionalProperties":false,"description":"Annotation event field mapping. Defines how to map a data frame field to an annotation event field.","properties":{"regex":{"description":"Regular expression to apply to the field value","type":"string"},"source":{"default":"field","description":"Source type for the field value","type":"string"},"value":{"description":"Constant value to use when source is \"text\"","type":"string"}},"type":"object"},"AnnotationPanelFilter":{"additionalProperties":false,"properties":{"exclude":{"default":false,"description":"Should the specified panels be included or excluded","type":"boolean"},"ids":{"description":"Panel IDs that should be included or excluded","items":{"type":"integer"},"type":"array"}},"required":["ids"],"type":"object"},"AnnotationQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"AnnotationQuery","type":"string"},"spec":{"$ref":"#/components/schemas/AnnotationQuerySpec"}},"required":["kind","spec"],"type":"object"},"AnnotationQueryPlacement":{"const":"inControlsMenu","description":"Annotation Query placement. Defines where the annotation query should be displayed.\n- \"inControlsMenu\" renders the annotation query in the dashboard controls dropdown menu","type":"string"},"AnnotationQuerySpec":{"additionalProperties":false,"properties":{"builtIn":{"default":false,"type":"boolean"},"enable":{"type":"boolean"},"filter":{"$ref":"#/components/schemas/AnnotationPanelFilter"},"hide":{"type":"boolean"},"iconColor":{"type":"string"},"legacyOptions":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"Catch-all field for datasource-specific properties. Should not be available in as code tooling.","type":"object"},"mappings":{"additionalProperties":{"$ref":"#/components/schemas/AnnotationEventFieldMapping"},"description":"Mappings define how to convert data frame fields to annotation event fields.","type":"object"},"name":{"type":"string"},"placement":{"$ref":"#/components/schemas/AnnotationQueryPlacement","description":"Placement can be used to display the annotation query somewhere else on the dashboard other than the default location."},"query":{"$ref":"#/components/schemas/DataQueryKind"}},"required":["query","enable","hide","iconColor","name"],"type":"object"},"AutoGridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutItemSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"element":{"$ref":"#/components/schemas/ElementReference"},"repeat":{"$ref":"#/components/schemas/AutoGridRepeatOptions"}},"required":["element"],"type":"object"},"AutoGridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"AutoGridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/AutoGridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"AutoGridLayoutSpec":{"additionalProperties":false,"properties":{"columnWidth":{"type":"number"},"columnWidthMode":{"default":"standard","enum":["narrow","standard","wide","custom"],"type":"string"},"fillScreen":{"default":false,"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/AutoGridLayoutItemKind"},"type":"array"},"maxColumnCount":{"default":3,"type":"number"},"rowHeight":{"type":"number"},"rowHeightMode":{"default":"standard","enum":["short","standard","tall","custom"],"type":"string"}},"required":["columnWidthMode","rowHeightMode","items"],"type":"object"},"AutoGridRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"ConditionalRenderingDataKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingData","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingDataSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingDataSpec":{"additionalProperties":false,"properties":{"value":{"type":"boolean"}},"required":["value"],"type":"object"},"ConditionalRenderingGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingGroup","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingGroupSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingGroupSpec":{"additionalProperties":false,"properties":{"condition":{"enum":["and","or"],"type":"string"},"items":{"items":{"oneOf":[{"$ref":"#/components/schemas/ConditionalRenderingVariableKind"},{"$ref":"#/components/schemas/ConditionalRenderingDataKind"},{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeKind"}]},"type":"array"},"visibility":{"enum":["show","hide"],"type":"string"}},"required":["visibility","condition","items"],"type":"object"},"ConditionalRenderingTimeRangeSizeKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingTimeRangeSize","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingTimeRangeSizeSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingTimeRangeSizeSpec":{"additionalProperties":false,"properties":{"value":{"type":"string"}},"required":["value"],"type":"object"},"ConditionalRenderingVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"ConditionalRenderingVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConditionalRenderingVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConditionalRenderingVariableSpec":{"additionalProperties":false,"properties":{"operator":{"enum":["equals","notEquals","matches","notMatches"],"type":"string"},"value":{"type":"string"},"variable":{"type":"string"}},"required":["variable","operator","value"],"type":"object"},"ConstantVariableKind":{"additionalProperties":false,"description":"Constant variable kind","properties":{"kind":{"const":"ConstantVariable","type":"string"},"spec":{"$ref":"#/components/schemas/ConstantVariableSpec"}},"required":["kind","spec"],"type":"object"},"ConstantVariableSpec":{"additionalProperties":false,"description":"Constant variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","hide","skipUrlSync"],"type":"object"},"ConversionStatus":{"additionalProperties":false,"description":"ConversionStatus is the status of the conversion of the dashboard.","properties":{"error":{"description":"The error message from the conversion.\nEmpty if the conversion has not failed.","type":"string"},"failed":{"description":"Whether from another version has failed.\nIf true, means that the dashboard is not valid,\nand the caller should instead fetch the stored version.","type":"boolean"},"source":{"additionalProperties":{},"description":"The original value map[string]any","type":"object"},"storedVersion":{"description":"The version which was stored when the dashboard was created / updated.\nFetching this version should always succeed.","type":"string"}},"required":["failed"],"type":"object"},"CustomVariableKind":{"additionalProperties":false,"description":"Custom variable kind","properties":{"kind":{"const":"CustomVariable","type":"string"},"spec":{"$ref":"#/components/schemas/CustomVariableSpec"}},"required":["kind","spec"],"type":"object"},"CustomVariableSpec":{"additionalProperties":false,"description":"Custom variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"Dashboard":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DashboardCursorSync":{"description":"\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip.","enum":["Crosshair","Tooltip","Off"],"type":"string"},"DashboardLink":{"additionalProperties":false,"description":"Links with references to other dashboards or external resources","properties":{"asDropdown":{"default":false,"description":"If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards","type":"boolean"},"icon":{"description":"Icon name to be displayed with the link","type":"string"},"includeVars":{"default":false,"description":"If true, includes current template variables values in the link as query params","type":"boolean"},"keepTime":{"default":false,"description":"If true, includes current time range in the link as query params","type":"boolean"},"placement":{"$ref":"#/components/schemas/DashboardLinkPlacement","description":"Placement can be used to display the link somewhere else on the dashboard other than above the visualisations."},"tags":{"description":"List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards","items":{"type":"string"},"type":"array"},"targetBlank":{"default":false,"description":"If true, the link will be opened in a new tab","type":"boolean"},"title":{"description":"Title to display with the link","type":"string"},"tooltip":{"description":"Tooltip to display when the user hovers their mouse over it","type":"string"},"type":{"$ref":"#/components/schemas/DashboardLinkType","description":"Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)\nFIXME: The type is generated as ` + "`" + `type: DashboardLinkType | dashboardLinkType.Link;` + "`" + ` but it should be ` + "`" + `type: DashboardLinkType` + "`" + `"},"url":{"description":"Link URL. Only required/valid if the type is link","type":"string"}},"required":["title","type","icon","tooltip","tags","asDropdown","targetBlank","includeVars","keepTime"],"type":"object"},"DashboardLinkPlacement":{"const":"inControlsMenu","description":"Dashboard Link placement. Defines where the link should be displayed.\n- \"inControlsMenu\" renders the link in bottom part of the dashboard controls dropdown menu","type":"string"},"DashboardLinkType":{"description":"Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)","enum":["link","dashboards"],"type":"string"},"DataLink":{"additionalProperties":false,"properties":{"targetBlank":{"type":"boolean"},"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"],"type":"object"},"DataQueryKind":{"additionalProperties":false,"properties":{"datasource":{"additionalProperties":false,"description":"New type for datasource reference\nNot creating a new type until we figure out how to handle DS refs for group by, adhoc, and every place that uses DataSourceRef in TS.","properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"DataQuery","type":"string"},"spec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"version":{"default":"v0","type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"DataTopic":{"description":"A topic is attached to DataFrame metadata in query results.\nThis specifies where the data should be used.","enum":["series","annotations","alertStates"],"type":"string"},"DataTransformerConfig":{"additionalProperties":false,"description":"Transformations allow to manipulate data returned by a query before the system applies a visualization.\nUsing transformations you can: rename fields, join time series data, perform mathematical operations across queries,\nuse the output of one transformation as the input to another transformation, etc.","properties":{"disabled":{"description":"Disabled transformations are skipped","type":"boolean"},"filter":{"$ref":"#/components/schemas/MatcherConfig","description":"Optional frame matcher. When missing it will be applied to all results"},"id":{"description":"Unique identifier of transformer","type":"string"},"options":{"additionalProperties":{},"description":"Options to be passed to the transformer\nValid options depend on the transformer id","type":"object"},"topic":{"$ref":"#/components/schemas/DataTopic","description":"Where to pull DataFrames from as input to transformation"}},"required":["id","options"],"type":"object"},"DatasourceVariableKind":{"additionalProperties":false,"description":"Datasource variable kind","properties":{"kind":{"const":"DatasourceVariable","type":"string"},"spec":{"$ref":"#/components/schemas/DatasourceVariableSpec"}},"required":["kind","spec"],"type":"object"},"DatasourceVariableSpec":{"additionalProperties":false,"description":"Datasource variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"pluginId":{"default":"","type":"string"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","pluginId","refresh","regex","current","options","multi","includeAll","hide","skipUrlSync","allowCustomValue"],"type":"object"},"DynamicConfigValue":{"additionalProperties":false,"properties":{"id":{"default":"","type":"string"},"value":{"additionalProperties":{},"type":"object"}},"required":["id"],"type":"object"},"Element":{"description":"Supported dashboard elements\n|* more element types in the future","oneOf":[{"$ref":"#/components/schemas/PanelKind"},{"$ref":"#/components/schemas/LibraryPanelKind"}]},"ElementReference":{"additionalProperties":false,"properties":{"kind":{"const":"ElementReference","type":"string"},"name":{"type":"string"}},"required":["kind","name"],"type":"object"},"FetchOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url"],"type":"object"},"FieldColor":{"additionalProperties":false,"description":"Map a field to a color.","properties":{"fixedColor":{"description":"The fixed color value for fixed or shades color modes.","type":"string"},"mode":{"$ref":"#/components/schemas/FieldColorModeId","description":"The main color scheme mode."},"seriesBy":{"$ref":"#/components/schemas/FieldColorSeriesByMode","description":"Some visualizations need to know how to assign a series color from by value color schemes."}},"required":["mode"],"type":"object"},"FieldColorModeId":{"description":"Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.\nContinuous color interpolates a color using the percentage of a value relative to min and max.\nAccepted values are:\n` + "`" + `thresholds` + "`" + `: From thresholds. Informs Grafana to take the color from the matching threshold\n` + "`" + `palette-classic` + "`" + `: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `palette-classic-by-name` + "`" + `: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations\n` + "`" + `continuous-viridis` + "`" + `: Continuous Viridis palette mode\n` + "`" + `continuous-magma` + "`" + `: Continuous Magma palette mode\n` + "`" + `continuous-plasma` + "`" + `: Continuous Plasma palette mode\n` + "`" + `continuous-inferno` + "`" + `: Continuous Inferno palette mode\n` + "`" + `continuous-cividis` + "`" + `: Continuous Cividis palette mode\n` + "`" + `continuous-GrYlRd` + "`" + `: Continuous Green-Yellow-Red palette mode\n` + "`" + `continuous-RdYlGr` + "`" + `: Continuous Red-Yellow-Green palette mode\n` + "`" + `continuous-BlYlRd` + "`" + `: Continuous Blue-Yellow-Red palette mode\n` + "`" + `continuous-YlRd` + "`" + `: Continuous Yellow-Red palette mode\n` + "`" + `continuous-BlPu` + "`" + `: Continuous Blue-Purple palette mode\n` + "`" + `continuous-YlBl` + "`" + `: Continuous Yellow-Blue palette mode\n` + "`" + `continuous-blues` + "`" + `: Continuous Blue palette mode\n` + "`" + `continuous-reds` + "`" + `: Continuous Red palette mode\n` + "`" + `continuous-greens` + "`" + `: Continuous Green palette mode\n` + "`" + `continuous-purples` + "`" + `: Continuous Purple palette mode\n` + "`" + `shades` + "`" + `: Shades of a single color. Specify a single color, useful in an override rule.\n` + "`" + `fixed` + "`" + `: Fixed color mode. Specify a single color, useful in an override rule.","enum":["thresholds","palette-classic","palette-classic-by-name","continuous-viridis","continuous-magma","continuous-plasma","continuous-inferno","continuous-cividis","continuous-GrYlRd","continuous-RdYlGr","continuous-BlYlRd","continuous-YlRd","continuous-BlPu","continuous-YlBl","continuous-blues","continuous-reds","continuous-greens","continuous-purples","fixed","shades"],"type":"string"},"FieldColorSeriesByMode":{"description":"Defines how to assign a series color from \"by value\" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.","enum":["min","max","last"],"type":"string"},"FieldConfig":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"actions":{"description":"Define interactive HTTP requests that can be triggered from data visualizations.","items":{"$ref":"#/components/schemas/Action"},"type":"array"},"color":{"$ref":"#/components/schemas/FieldColor","description":"Panel color configuration"},"custom":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"custom is specified by the FieldConfig field\nin panel plugin schemas.","type":"object"},"decimals":{"description":"Specify the number of decimals Grafana includes in the rendered value.\nIf you leave this field blank, Grafana automatically truncates the number of decimals based on the value.\nFor example 1.1234 will display as 1.12 and 100.456 will display as 100.\nTo display all decimals, set the unit to ` + "`" + `String` + "`" + `.","type":"number"},"description":{"description":"Human readable field metadata","type":"string"},"displayName":{"description":"The display value for this field. This supports template variables blank is auto","type":"string"},"displayNameFromDS":{"description":"This can be used by data sources that return and explicit naming structure for values and labels\nWhen this property is configured, this value is used rather than the default naming strategy.","type":"string"},"filterable":{"description":"True if data source field supports ad-hoc filters","type":"boolean"},"links":{"description":"The behavior when clicking on a result","items":{"additionalProperties":{},"type":"object"},"type":"array"},"mappings":{"description":"Convert input values into a display string","items":{"$ref":"#/components/schemas/ValueMapping"},"type":"array"},"max":{"description":"The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"min":{"description":"The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.","type":"number"},"noValue":{"description":"Alternative to empty string","type":"string"},"path":{"description":"An explicit path to the field in the datasource. When the frame meta includes a path,\nThis will default to ` + "`" + `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used to update the results","type":"string"},"thresholds":{"$ref":"#/components/schemas/ThresholdsConfig","description":"Map numeric values to states"},"unit":{"description":"Unit a field should use. The unit you select is applied to all fields except time.\nYou can use the units ID availables in Grafana or a custom unit.\nAvailable units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts\nAs custom unit, you can use the following formats:\n` + "`" + `suffix:\u003csuffix\u003e` + "`" + ` for custom unit that should go after value.\n` + "`" + `prefix:\u003cprefix\u003e` + "`" + ` for custom unit that should go before value.\n` + "`" + `time:\u003cformat\u003e` + "`" + ` For custom date time formats type for example ` + "`" + `time:YYYY-MM-DD` + "`" + `.\n` + "`" + `si:\u003cbase scale\u003e\u003cunit characters\u003e` + "`" + ` for custom SI units. For example: ` + "`" + `si: mF` + "`" + `. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character.\n` + "`" + `count:\u003cunit\u003e` + "`" + ` for a custom count unit.\n` + "`" + `currency:\u003cunit\u003e` + "`" + ` for custom a currency unit.","type":"string"},"writeable":{"description":"True if data source can write a value to the path. Auth/authz are supported separately","type":"boolean"}},"type":"object"},"FieldConfigSource":{"additionalProperties":false,"description":"The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.\nEach column within this structure is called a field. A field can represent a single time series or table column.\nField options allow you to change how the data is displayed in your visualizations.","properties":{"defaults":{"$ref":"#/components/schemas/FieldConfig","description":"Defaults are the options applied to all fields."},"overrides":{"description":"Overrides are the options applied to specific fields overriding the defaults.","items":{"additionalProperties":false,"properties":{"__systemRef":{"description":"Describes config override rules created when interacting with Grafana.","type":"string"},"matcher":{"$ref":"#/components/schemas/MatcherConfig"},"properties":{"items":{"$ref":"#/components/schemas/DynamicConfigValue"},"type":"array"}},"required":["matcher","properties"],"type":"object"},"type":"array"}},"required":["defaults","overrides"],"type":"object"},"FilterOrigin":{"const":"dashboard","description":"Determine the origin of the adhoc variable filter","type":"string"},"GridLayoutItemKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayoutItem","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutItemSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutItemSpec":{"additionalProperties":false,"properties":{"element":{"$ref":"#/components/schemas/ElementReference","description":"reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference"},"height":{"type":"integer"},"repeat":{"$ref":"#/components/schemas/RepeatOptions"},"width":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"}},"required":["x","y","width","height","element"],"type":"object"},"GridLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"GridLayout","type":"string"},"spec":{"$ref":"#/components/schemas/GridLayoutSpec"}},"required":["kind","spec"],"type":"object"},"GridLayoutSpec":{"additionalProperties":false,"properties":{"items":{"items":{"$ref":"#/components/schemas/GridLayoutItemKind"},"type":"array"}},"required":["items"],"type":"object"},"GroupByVariableKind":{"additionalProperties":false,"description":"Group variable kind","properties":{"datasource":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"type":"object"},"group":{"type":"string"},"kind":{"const":"GroupByVariable","type":"string"},"spec":{"$ref":"#/components/schemas/GroupByVariableSpec"}},"required":["kind","group","spec"],"type":"object"},"GroupByVariableSpec":{"additionalProperties":false,"description":"GroupBy variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"defaultValue":{"$ref":"#/components/schemas/VariableOption"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","options","multi","hide","skipUrlSync"],"type":"object"},"HttpRequestMethod":{"enum":["GET","PUT","POST","DELETE","PATCH"],"type":"string"},"InfinityOptions":{"additionalProperties":false,"properties":{"body":{"type":"string"},"datasourceUid":{"type":"string"},"headers":{"items":{"items":{"type":"string"},"type":"array"},"type":"array"},"method":{"$ref":"#/components/schemas/HttpRequestMethod"},"queryParams":{"description":"These are 2D arrays of strings, each representing a key-value pair\nWe are defining them this way because we can't generate a go struct that\nthat would have exactly two strings in each sub-array","items":{"items":{"type":"string"},"type":"array"},"type":"array"},"url":{"type":"string"}},"required":["method","url","datasourceUid"],"type":"object"},"IntervalVariableKind":{"additionalProperties":false,"description":"Interval variable kind","properties":{"kind":{"const":"IntervalVariable","type":"string"},"spec":{"$ref":"#/components/schemas/IntervalVariableSpec"}},"required":["kind","spec"],"type":"object"},"IntervalVariableSpec":{"additionalProperties":false,"description":"Interval variable specification","properties":{"auto":{"default":false,"type":"boolean"},"auto_count":{"default":0,"type":"integer"},"auto_min":{"default":"","type":"string"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"query":{"default":"","type":"string"},"refresh":{"const":"onTimeRangeChanged","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","query","current","options","auto","auto_min","auto_count","refresh","hide","skipUrlSync"],"type":"object"},"LibraryPanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"LibraryPanel","type":"string"},"spec":{"$ref":"#/components/schemas/LibraryPanelKindSpec"}},"required":["kind","spec"],"type":"object"},"LibraryPanelKindSpec":{"additionalProperties":false,"properties":{"id":{"description":"Panel ID for the library panel in the dashboard","type":"number"},"libraryPanel":{"$ref":"#/components/schemas/LibraryPanelRef"},"title":{"description":"Title for the library panel in the dashboard","type":"string"}},"required":["id","title","libraryPanel"],"type":"object"},"LibraryPanelRef":{"additionalProperties":false,"description":"A library panel is a reusable panel that you can use in any dashboard.\nWhen you make a change to a library panel, that change propagates to all instances of where the panel is used.\nLibrary panels streamline reuse of panels across multiple dashboards.","properties":{"name":{"description":"Library panel name","type":"string"},"uid":{"description":"Library panel uid","type":"string"}},"required":["name","uid"],"type":"object"},"MappingType":{"description":"Supported value mapping types\n` + "`" + `value` + "`" + `: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.\n` + "`" + `range` + "`" + `: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.\n` + "`" + `regex` + "`" + `: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.\n` + "`" + `special` + "`" + `: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A.","enum":["value","range","regex","special"],"type":"string"},"MatcherConfig":{"additionalProperties":false,"description":"Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.\nIt comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.","properties":{"id":{"default":"","description":"The matcher id. This is used to find the matcher implementation from registry.","type":"string"},"options":{"additionalProperties":{},"description":"The matcher options. This is specific to the matcher implementation.","type":"object"}},"required":["id"],"type":"object"},"MetricFindValue":{"additionalProperties":false,"description":"Define the MetricFindValue type","properties":{"expandable":{"type":"boolean"},"group":{"type":"string"},"text":{"type":"string"},"value":{"oneOf":[{"type":"string"},{"type":"number"}]}},"required":["text"],"type":"object"},"PanelKind":{"additionalProperties":false,"properties":{"kind":{"const":"Panel","type":"string"},"spec":{"$ref":"#/components/schemas/PanelSpec"}},"required":["kind","spec"],"type":"object"},"PanelQueryKind":{"additionalProperties":false,"properties":{"kind":{"const":"PanelQuery","type":"string"},"spec":{"$ref":"#/components/schemas/PanelQuerySpec"}},"required":["kind","spec"],"type":"object"},"PanelQuerySpec":{"additionalProperties":false,"properties":{"hidden":{"type":"boolean"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refId":{"default":"A","type":"string"}},"required":["query","refId","hidden"],"type":"object"},"PanelSpec":{"additionalProperties":false,"properties":{"data":{"$ref":"#/components/schemas/QueryGroupKind"},"description":{"type":"string"},"id":{"type":"number"},"links":{"items":{"$ref":"#/components/schemas/DataLink"},"type":"array"},"title":{"type":"string"},"transparent":{"type":"boolean"},"vizConfig":{"$ref":"#/components/schemas/VizConfigKind"}},"required":["id","title","description","links","data","vizConfig"],"type":"object"},"QueryGroupKind":{"additionalProperties":false,"properties":{"kind":{"const":"QueryGroup","type":"string"},"spec":{"$ref":"#/components/schemas/QueryGroupSpec"}},"required":["kind","spec"],"type":"object"},"QueryGroupSpec":{"additionalProperties":false,"properties":{"queries":{"items":{"$ref":"#/components/schemas/PanelQueryKind"},"type":"array"},"queryOptions":{"$ref":"#/components/schemas/QueryOptionsSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationKind"},"type":"array"}},"required":["queries","transformations","queryOptions"],"type":"object"},"QueryOptionsSpec":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"string"},"hideTimeOverride":{"type":"boolean"},"interval":{"type":"string"},"maxDataPoints":{"type":"integer"},"queryCachingTTL":{"type":"integer"},"timeCompare":{"type":"string"},"timeFrom":{"type":"string"},"timeShift":{"type":"string"}},"type":"object"},"QueryVariableKind":{"additionalProperties":false,"description":"Query variable kind","properties":{"kind":{"const":"QueryVariable","type":"string"},"spec":{"$ref":"#/components/schemas/QueryVariableSpec"}},"required":["kind","spec"],"type":"object"},"QueryVariableSpec":{"additionalProperties":false,"description":"Query variable specification","properties":{"allValue":{"type":"string"},"allowCustomValue":{"default":true,"type":"boolean"},"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"definition":{"type":"string"},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"includeAll":{"default":false,"type":"boolean"},"label":{"type":"string"},"multi":{"default":false,"type":"boolean"},"name":{"default":"","type":"string"},"options":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"placeholder":{"type":"string"},"query":{"$ref":"#/components/schemas/DataQueryKind"},"refresh":{"$ref":"#/components/schemas/VariableRefresh","default":"never"},"regex":{"default":"","type":"string"},"regexApplyTo":{"$ref":"#/components/schemas/VariableRegexApplyTo","default":"value"},"skipUrlSync":{"default":false,"type":"boolean"},"sort":{"$ref":"#/components/schemas/VariableSort"},"staticOptions":{"items":{"$ref":"#/components/schemas/VariableOption"},"type":"array"},"staticOptionsOrder":{"enum":["before","after","sorted"],"type":"string"}},"required":["name","current","hide","refresh","skipUrlSync","query","regex","sort","options","multi","includeAll","allowCustomValue"],"type":"object"},"RangeMap":{"additionalProperties":false,"description":"Maps numerical ranges to a display text and color.\nFor example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number.","properties":{"options":{"additionalProperties":false,"description":"Range to match against and the result to apply when the value is within the range","properties":{"from":{"description":"Min value of the range. It can be null which means -Infinity","type":"number"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value is within the range"},"to":{"description":"Max value of the range. It can be null which means +Infinity","type":"number"}},"required":["from","to","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RegexMap":{"additionalProperties":false,"description":"Maps regular expressions to replacement text and a color.\nFor example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain.","properties":{"options":{"additionalProperties":false,"description":"Regular expression to match against and the result to apply when the value matches the regex","properties":{"pattern":{"description":"Regular expression to match against","type":"string"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the regex"}},"required":["pattern","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"RepeatMode":{"const":"variable","description":"other repeat modes will be added in the future: label, frame","type":"string"},"RepeatOptions":{"additionalProperties":false,"properties":{"direction":{"enum":["h","v"],"type":"string"},"maxPerRow":{"type":"integer"},"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"RowsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowKind":{"additionalProperties":false,"properties":{"kind":{"const":"RowsLayoutRow","type":"string"},"spec":{"$ref":"#/components/schemas/RowsLayoutRowSpec"}},"required":["kind","spec"],"type":"object"},"RowsLayoutRowSpec":{"additionalProperties":false,"properties":{"collapse":{"type":"boolean"},"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"fillScreen":{"type":"boolean"},"hideHeader":{"type":"boolean"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/RowRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"RowsLayoutSpec":{"additionalProperties":false,"properties":{"rows":{"items":{"$ref":"#/components/schemas/RowsLayoutRowKind"},"type":"array"}},"required":["rows"],"type":"object"},"SpecialValueMap":{"additionalProperties":false,"description":"Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color.\nSee SpecialValueMatch to see the list of special values.\nFor example, you can configure a special value mapping so that null values appear as N/A.","properties":{"options":{"additionalProperties":false,"properties":{"match":{"$ref":"#/components/schemas/SpecialValueMatch","description":"Special value to match against"},"result":{"$ref":"#/components/schemas/ValueMappingResult","description":"Config to apply when the value matches the special value"}},"required":["match","result"],"type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"SpecialValueMatch":{"description":"Special value types supported by the ` + "`" + `SpecialValueMap` + "`" + `","enum":["true","false","null","nan","null+nan","empty"],"type":"string"},"SwitchVariableKind":{"additionalProperties":false,"properties":{"kind":{"const":"SwitchVariable","type":"string"},"spec":{"$ref":"#/components/schemas/SwitchVariableSpec"}},"required":["kind","spec"],"type":"object"},"SwitchVariableSpec":{"additionalProperties":false,"properties":{"current":{"default":"false","type":"string"},"description":{"type":"string"},"disabledValue":{"default":"false","type":"string"},"enabledValue":{"default":"true","type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","enabledValue","disabledValue","hide","skipUrlSync"],"type":"object"},"TabRepeatOptions":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/RepeatMode"},"value":{"type":"string"}},"required":["mode","value"],"type":"object"},"TabsLayoutKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayout","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutSpec":{"additionalProperties":false,"properties":{"tabs":{"items":{"$ref":"#/components/schemas/TabsLayoutTabKind"},"type":"array"}},"required":["tabs"],"type":"object"},"TabsLayoutTabKind":{"additionalProperties":false,"properties":{"kind":{"const":"TabsLayoutTab","type":"string"},"spec":{"$ref":"#/components/schemas/TabsLayoutTabSpec"}},"required":["kind","spec"],"type":"object"},"TabsLayoutTabSpec":{"additionalProperties":false,"properties":{"conditionalRendering":{"$ref":"#/components/schemas/ConditionalRenderingGroupKind"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"repeat":{"$ref":"#/components/schemas/TabRepeatOptions"},"title":{"type":"string"}},"required":["layout"],"type":"object"},"TextVariableKind":{"additionalProperties":false,"description":"Text variable kind","properties":{"kind":{"const":"TextVariable","type":"string"},"spec":{"$ref":"#/components/schemas/TextVariableSpec"}},"required":["kind","spec"],"type":"object"},"TextVariableSpec":{"additionalProperties":false,"description":"Text variable specification","properties":{"current":{"$ref":"#/components/schemas/VariableOption","default":{"text":"","value":""}},"description":{"type":"string"},"hide":{"$ref":"#/components/schemas/VariableHide","default":"dontHide"},"label":{"type":"string"},"name":{"default":"","type":"string"},"query":{"default":"","type":"string"},"skipUrlSync":{"default":false,"type":"boolean"}},"required":["name","current","query","hide","skipUrlSync"],"type":"object"},"Threshold":{"additionalProperties":false,"properties":{"color":{"type":"string"},"value":{"description":"Value null means -Infinity","type":"number"}},"required":["value","color"],"type":"object"},"ThresholdsConfig":{"additionalProperties":false,"properties":{"mode":{"$ref":"#/components/schemas/ThresholdsMode"},"steps":{"items":{"$ref":"#/components/schemas/Threshold"},"type":"array"}},"required":["mode","steps"],"type":"object"},"ThresholdsMode":{"enum":["absolute","percentage"],"type":"string"},"TimeRangeOption":{"additionalProperties":false,"properties":{"display":{"default":"Last 6 hours","type":"string"},"from":{"default":"now-6h","type":"string"},"to":{"default":"now","type":"string"}},"required":["display","from","to"],"type":"object"},"TimeSettingsSpec":{"additionalProperties":false,"description":"Time configuration\nIt defines the default time config for the time picker, the refresh picker for the specific dashboard.","properties":{"autoRefresh":{"default":"","description":"Refresh rate of dashboard. Represented via interval string, e.g. \"5s\", \"1m\", \"1h\", \"1d\".\nv1: refresh","type":"string"},"autoRefreshIntervals":{"default":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"description":"Interval options available in the refresh picker dropdown.\nv1: timepicker.refresh_intervals","items":{"type":"string"},"type":"array"},"fiscalYearStartMonth":{"default":0,"description":"The month that the fiscal year starts on. 0 = January, 11 = December","type":"integer"},"from":{"default":"now-6h","description":"Start time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"hideTimepicker":{"default":false,"description":"Whether timepicker is visible or not.\nv1: timepicker.hidden","type":"boolean"},"nowDelay":{"description":"Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values.\nv1: timepicker.nowDelay","type":"string"},"quickRanges":{"description":"Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard.\nv1: timepicker.quick_ranges , not exposed in the UI","items":{"$ref":"#/components/schemas/TimeRangeOption"},"type":"array"},"timezone":{"default":"browser","description":"Timezone of dashboard. Accepted values are IANA TZDB zone ID or \"browser\" or \"utc\".","type":"string"},"to":{"default":"now","description":"End time range for dashboard.\nAccepted values are relative time strings like \"now-6h\" or absolute time strings like \"2020-07-10T08:00:00.000Z\".","type":"string"},"weekStart":{"description":"Day when the week starts. Expressed by the name of the day in lowercase, e.g. \"monday\".","enum":["saturday","monday","sunday"],"type":"string"}},"required":["from","to","autoRefresh","autoRefreshIntervals","hideTimepicker","fiscalYearStartMonth"],"type":"object"},"TransformationKind":{"additionalProperties":false,"properties":{"kind":{"description":"The kind of a TransformationKind is the transformation ID","type":"string"},"spec":{"$ref":"#/components/schemas/DataTransformerConfig"}},"required":["kind","spec"],"type":"object"},"ValueMap":{"additionalProperties":false,"description":"Maps text values to a color or different display text and color.\nFor example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number.","properties":{"options":{"additionalProperties":{"$ref":"#/components/schemas/ValueMappingResult"},"description":"Map with \u003cvalue_to_match\u003e: ValueMappingResult. For example: { \"10\": { text: \"Perfection!\", color: \"green\" } }","type":"object"},"type":{"$ref":"#/components/schemas/MappingType"}},"required":["type","options"],"type":"object"},"ValueMapping":{"oneOf":[{"$ref":"#/components/schemas/ValueMap"},{"$ref":"#/components/schemas/RangeMap"},{"$ref":"#/components/schemas/RegexMap"},{"$ref":"#/components/schemas/SpecialValueMap"}]},"ValueMappingResult":{"additionalProperties":false,"description":"Result used as replacement with text and color when the value matches","properties":{"color":{"description":"Text to use when the value matches","type":"string"},"icon":{"description":"Icon to display when the value matches. Only specific visualizations.","type":"string"},"index":{"description":"Position in the mapping array. Only used internally.","type":"integer"},"text":{"description":"Text to display when the value matches","type":"string"}},"type":"object"},"VariableHide":{"description":"Determine if the variable shows on dashboard\nAccepted values are ` + "`" + `dontHide` + "`" + ` (show label and value), ` + "`" + `hideLabel` + "`" + ` (show value only), ` + "`" + `hideVariable` + "`" + ` (show nothing), ` + "`" + `inControlsMenu` + "`" + ` (show in a drop-down menu).","enum":["dontHide","hideLabel","hideVariable","inControlsMenu"],"type":"string"},"VariableKind":{"oneOf":[{"$ref":"#/components/schemas/QueryVariableKind"},{"$ref":"#/components/schemas/TextVariableKind"},{"$ref":"#/components/schemas/ConstantVariableKind"},{"$ref":"#/components/schemas/DatasourceVariableKind"},{"$ref":"#/components/schemas/IntervalVariableKind"},{"$ref":"#/components/schemas/CustomVariableKind"},{"$ref":"#/components/schemas/GroupByVariableKind"},{"$ref":"#/components/schemas/AdhocVariableKind"},{"$ref":"#/components/schemas/SwitchVariableKind"}]},"VariableOption":{"additionalProperties":false,"description":"Variable option specification","properties":{"selected":{"description":"Whether the option is selected or not","type":"boolean"},"text":{"description":"Text to be displayed for the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"value":{"description":"Value of the option","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]}},"required":["text","value"],"type":"object"},"VariableRefresh":{"description":"Options to config when to refresh a variable\n` + "`" + `never` + "`" + `: Never refresh the variable\n` + "`" + `onDashboardLoad` + "`" + `: Queries the data source every time the dashboard loads.\n` + "`" + `onTimeRangeChanged` + "`" + `: Queries the data source when the dashboard time range changes.","enum":["never","onDashboardLoad","onTimeRangeChanged"],"type":"string"},"VariableRegexApplyTo":{"description":"Determine whether regex applies to variable value or display text\nAccepted values are ` + "`" + `value` + "`" + ` (apply to value used in queries) or ` + "`" + `text` + "`" + ` (apply to display text shown to users)","enum":["value","text"],"type":"string"},"VariableSort":{"description":"Sort variable options\nAccepted values are:\n` + "`" + `disabled` + "`" + `: No sorting\n` + "`" + `alphabeticalAsc` + "`" + `: Alphabetical ASC\n` + "`" + `alphabeticalDesc` + "`" + `: Alphabetical DESC\n` + "`" + `numericalAsc` + "`" + `: Numerical ASC\n` + "`" + `numericalDesc` + "`" + `: Numerical DESC\n` + "`" + `alphabeticalCaseInsensitiveAsc` + "`" + `: Alphabetical Case Insensitive ASC\n` + "`" + `alphabeticalCaseInsensitiveDesc` + "`" + `: Alphabetical Case Insensitive DESC\n` + "`" + `naturalAsc` + "`" + `: Natural ASC\n` + "`" + `naturalDesc` + "`" + `: Natural DESC\nVariableSort enum with default value","enum":["disabled","alphabeticalAsc","alphabeticalDesc","numericalAsc","numericalDesc","alphabeticalCaseInsensitiveAsc","alphabeticalCaseInsensitiveDesc","naturalAsc","naturalDesc"],"type":"string"},"VizConfigKind":{"additionalProperties":false,"properties":{"group":{"description":"The group is the plugin ID","type":"string"},"kind":{"const":"VizConfig","type":"string"},"spec":{"$ref":"#/components/schemas/VizConfigSpec"},"version":{"type":"string"}},"required":["kind","group","version","spec"],"type":"object"},"VizConfigSpec":{"additionalProperties":false,"description":"--- Kinds ---","properties":{"fieldConfig":{"$ref":"#/components/schemas/FieldConfigSource"},"options":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}},"required":["options","fieldConfig"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"annotations":{"items":{"$ref":"#/components/schemas/AnnotationQueryKind"},"type":"array"},"cursorSync":{"$ref":"#/components/schemas/DashboardCursorSync","default":"Off","description":"Configuration of dashboard cursor sync behavior.\n\"Off\" for no shared crosshair or tooltip (default).\n\"Crosshair\" for shared crosshair.\n\"Tooltip\" for shared crosshair AND shared tooltip."},"description":{"description":"Description of dashboard.","type":"string"},"editable":{"default":true,"description":"Whether a dashboard is editable or not.","type":"boolean"},"elements":{"additionalProperties":{"$ref":"#/components/schemas/Element"},"type":"object"},"layout":{"oneOf":[{"$ref":"#/components/schemas/GridLayoutKind"},{"$ref":"#/components/schemas/RowsLayoutKind"},{"$ref":"#/components/schemas/AutoGridLayoutKind"},{"$ref":"#/components/schemas/TabsLayoutKind"}]},"links":{"description":"Links with references to other dashboards or external websites.","items":{"$ref":"#/components/schemas/DashboardLink"},"type":"array"},"liveNow":{"description":"When set to true, the dashboard will redraw panels at an interval matching the pixel width.\nThis will keep data \"moving left\" regardless of the query refresh rate. This setting helps\navoid dashboards presenting stale live data.","type":"boolean"},"preload":{"default":false,"description":"When set to true, the dashboard will load all panels in the dashboard when it's loaded.","type":"boolean"},"revision":{"description":"Plugins only. The version of the dashboard installed together with the plugin.\nThis is used to determine if the dashboard should be updated when the plugin is updated.","type":"integer"},"tags":{"description":"Tags associated with dashboard.","items":{"type":"string"},"type":"array"},"timeSettings":{"$ref":"#/components/schemas/TimeSettingsSpec"},"title":{"description":"Title of dashboard.","type":"string"},"variables":{"description":"Configured template variables.","items":{"$ref":"#/components/schemas/VariableKind"},"type":"array"}},"required":["annotations","cursorSync","elements","layout","links","preload","tags","timeSettings","title","variables"],"type":"object"},"status":{"additionalProperties":false,"properties":{"conversion":{"$ref":"#/components/schemas/ConversionStatus","description":"Optional conversion status."}},"type":"object"}}`) versionSchemaDashboardv2beta1 app.VersionSchema _ = json.Unmarshal(rawSchemaDashboardv2beta1, &versionSchemaDashboardv2beta1) ) diff --git a/apps/dashboard/pkg/migration/conversion/conversion.go b/apps/dashboard/pkg/migration/conversion/conversion.go index d0f90e1ba98..ad65dc84c85 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion.go +++ b/apps/dashboard/pkg/migration/conversion/conversion.go @@ -12,13 +12,6 @@ import ( ) func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error { - // Wrap the provider once with 10s caching for all conversions. - // This prevents repeated DB queries across multiple conversion calls while allowing - // the cache to refresh periodically, making it suitable for long-lived singleton usage. - dsIndexProvider = schemaversion.WrapIndexProviderWithCache(dsIndexProvider) - // Wrap library element provider with caching as well - leIndexProvider = schemaversion.WrapLibraryElementProviderWithCache(leIndexProvider) - // v0 conversions if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv1.Dashboard)(nil), withConversionMetrics(dashv0.APIVERSION, dashv1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { @@ -62,13 +55,13 @@ func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSo // v2alpha1 conversions if err := s.AddConversionFunc((*dashv2alpha1.Dashboard)(nil), (*dashv0.Dashboard)(nil), withConversionMetrics(dashv2alpha1.APIVERSION, dashv0.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V2alpha1_to_V0(a.(*dashv2alpha1.Dashboard), b.(*dashv0.Dashboard), scope, dsIndexProvider) + return Convert_V2alpha1_to_V0(a.(*dashv2alpha1.Dashboard), b.(*dashv0.Dashboard), scope) })); err != nil { return err } if err := s.AddConversionFunc((*dashv2alpha1.Dashboard)(nil), (*dashv1.Dashboard)(nil), withConversionMetrics(dashv2alpha1.APIVERSION, dashv1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error { - return Convert_V2alpha1_to_V1beta1(a.(*dashv2alpha1.Dashboard), b.(*dashv1.Dashboard), scope, dsIndexProvider) + return Convert_V2alpha1_to_V1beta1(a.(*dashv2alpha1.Dashboard), b.(*dashv1.Dashboard), scope) })); err != nil { return err } diff --git a/apps/dashboard/pkg/migration/conversion/conversion_cache_test.go b/apps/dashboard/pkg/migration/conversion/conversion_cache_test.go new file mode 100644 index 00000000000..3e54d9682fd --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/conversion_cache_test.go @@ -0,0 +1,454 @@ +package conversion + +import ( + "context" + "sync/atomic" + "testing" + "time" + + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" + "github.com/grafana/grafana/apps/dashboard/pkg/migration" + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// countingDataSourceProvider tracks how many times Index() is called +type countingDataSourceProvider struct { + datasources []schemaversion.DataSourceInfo + callCount atomic.Int64 +} + +func newCountingDataSourceProvider(datasources []schemaversion.DataSourceInfo) *countingDataSourceProvider { + return &countingDataSourceProvider{ + datasources: datasources, + } +} + +func (p *countingDataSourceProvider) Index(_ context.Context) *schemaversion.DatasourceIndex { + p.callCount.Add(1) + return schemaversion.NewDatasourceIndex(p.datasources) +} + +func (p *countingDataSourceProvider) getCallCount() int64 { + return p.callCount.Load() +} + +// countingLibraryElementProvider tracks how many times GetLibraryElementInfo() is called +type countingLibraryElementProvider struct { + elements []schemaversion.LibraryElementInfo + callCount atomic.Int64 +} + +func newCountingLibraryElementProvider(elements []schemaversion.LibraryElementInfo) *countingLibraryElementProvider { + return &countingLibraryElementProvider{ + elements: elements, + } +} + +func (p *countingLibraryElementProvider) GetLibraryElementInfo(_ context.Context) []schemaversion.LibraryElementInfo { + p.callCount.Add(1) + return p.elements +} + +func (p *countingLibraryElementProvider) getCallCount() int64 { + return p.callCount.Load() +} + +// createTestV0Dashboard creates a minimal v0 dashboard for testing +// The dashboard has a datasource with UID only (no type) to force provider lookup +// and includes library panels to test library element provider caching +func createTestV0Dashboard(namespace, title string) *dashv0.Dashboard { + return &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dashboard", + Namespace: namespace, + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": title, + "schemaVersion": schemaversion.LATEST_VERSION, + // Variables with datasource reference that requires lookup + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "name": "query_var", + "type": "query", + "query": "label_values(up, job)", + // Datasource with UID only - type needs to be looked up + "datasource": map[string]interface{}{ + "uid": "ds1", + // type is intentionally omitted to trigger provider lookup + }, + }, + }, + }, + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "title": "Test Panel", + "type": "timeseries", + "targets": []interface{}{ + map[string]interface{}{ + // Datasource with UID only - type needs to be looked up + "datasource": map[string]interface{}{ + "uid": "ds1", + }, + }, + }, + }, + // Library panel reference - triggers library element provider lookup + map[string]interface{}{ + "id": 2, + "title": "Library Panel with Horizontal Repeat", + "type": "library-panel-ref", + "gridPos": map[string]interface{}{ + "h": 8, + "w": 12, + "x": 0, + "y": 8, + }, + "libraryPanel": map[string]interface{}{ + "uid": "lib-panel-repeat-h", + "name": "Library Panel with Horizontal Repeat", + }, + }, + // Another library panel reference + map[string]interface{}{ + "id": 3, + "title": "Library Panel without Repeat", + "type": "library-panel-ref", + "gridPos": map[string]interface{}{ + "h": 3, + "w": 6, + "x": 0, + "y": 16, + }, + "libraryPanel": map[string]interface{}{ + "uid": "lib-panel-no-repeat", + "name": "Library Panel without Repeat", + }, + }, + }, + }, + }, + } +} + +// createTestV1Dashboard creates a minimal v1beta1 dashboard for testing +// The dashboard has a datasource with UID only (no type) to force provider lookup +// and includes library panels to test library element provider caching +func createTestV1Dashboard(namespace, title string) *dashv1.Dashboard { + return &dashv1.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dashboard", + Namespace: namespace, + }, + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "title": title, + "schemaVersion": schemaversion.LATEST_VERSION, + // Variables with datasource reference that requires lookup + "templating": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "name": "query_var", + "type": "query", + "query": "label_values(up, job)", + // Datasource with UID only - type needs to be looked up + "datasource": map[string]interface{}{ + "uid": "ds1", + // type is intentionally omitted to trigger provider lookup + }, + }, + }, + }, + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "title": "Test Panel", + "type": "timeseries", + "targets": []interface{}{ + map[string]interface{}{ + // Datasource with UID only - type needs to be looked up + "datasource": map[string]interface{}{ + "uid": "ds1", + }, + }, + }, + }, + // Library panel reference - triggers library element provider lookup + map[string]interface{}{ + "id": 2, + "title": "Library Panel with Vertical Repeat", + "type": "library-panel-ref", + "gridPos": map[string]interface{}{ + "h": 4, + "w": 6, + "x": 0, + "y": 8, + }, + "libraryPanel": map[string]interface{}{ + "uid": "lib-panel-repeat-v", + "name": "Library Panel with Vertical Repeat", + }, + }, + // Another library panel reference + map[string]interface{}{ + "id": 3, + "title": "Library Panel without Repeat", + "type": "library-panel-ref", + "gridPos": map[string]interface{}{ + "h": 3, + "w": 6, + "x": 6, + "y": 8, + }, + "libraryPanel": map[string]interface{}{ + "uid": "lib-panel-no-repeat", + "name": "Library Panel without Repeat", + }, + }, + }, + }, + }, + } +} + +// TestConversionCaching_V0_to_V2alpha1 verifies caching works when converting V0 to V2alpha1 +func TestConversionCaching_V0_to_V2alpha1(t *testing.T) { + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + elements := []schemaversion.LibraryElementInfo{ + {UID: "lib-panel-repeat-h", Name: "Library Panel with Horizontal Repeat", Type: "timeseries"}, + {UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"}, + } + + underlyingDS := newCountingDataSourceProvider(datasources) + underlyingLE := newCountingLibraryElementProvider(elements) + + cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute) + cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute) + + migration.ResetForTesting() + migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL) + + // Convert multiple dashboards in the same namespace + numDashboards := 5 + namespace := "default" + + for i := 0; i < numDashboards; i++ { + source := createTestV0Dashboard(namespace, "Dashboard "+string(rune('A'+i))) + target := &dashv2alpha1.Dashboard{} + + err := Convert_V0_to_V2alpha1(source, target, nil, cachedDS, cachedLE) + require.NoError(t, err, "conversion %d should succeed", i) + require.NotNil(t, target.Spec) + } + + // With caching, the underlying datasource provider should only be called once per namespace + // The test dashboard has datasources without type that require lookup + assert.Equal(t, int64(1), underlyingDS.getCallCount(), + "datasource provider should be called only once for %d conversions in same namespace", numDashboards) + // Library element provider should also be called only once per namespace due to caching + assert.Equal(t, int64(1), underlyingLE.getCallCount(), + "library element provider should be called only once for %d conversions in same namespace", numDashboards) +} + +// TestConversionCaching_V0_to_V2beta1 verifies caching works when converting V0 to V2beta1 +func TestConversionCaching_V0_to_V2beta1(t *testing.T) { + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + elements := []schemaversion.LibraryElementInfo{ + {UID: "lib-panel-repeat-h", Name: "Library Panel with Horizontal Repeat", Type: "timeseries"}, + {UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"}, + } + + underlyingDS := newCountingDataSourceProvider(datasources) + underlyingLE := newCountingLibraryElementProvider(elements) + + cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute) + cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute) + + migration.ResetForTesting() + migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL) + + numDashboards := 5 + namespace := "default" + + for i := 0; i < numDashboards; i++ { + source := createTestV0Dashboard(namespace, "Dashboard "+string(rune('A'+i))) + target := &dashv2beta1.Dashboard{} + + err := Convert_V0_to_V2beta1(source, target, nil, cachedDS, cachedLE) + require.NoError(t, err, "conversion %d should succeed", i) + require.NotNil(t, target.Spec) + } + + assert.Equal(t, int64(1), underlyingDS.getCallCount(), + "datasource provider should be called only once for %d conversions in same namespace", numDashboards) + assert.Equal(t, int64(1), underlyingLE.getCallCount(), + "library element provider should be called only once for %d conversions in same namespace", numDashboards) +} + +// TestConversionCaching_V1beta1_to_V2alpha1 verifies caching works when converting V1beta1 to V2alpha1 +func TestConversionCaching_V1beta1_to_V2alpha1(t *testing.T) { + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + elements := []schemaversion.LibraryElementInfo{ + {UID: "lib-panel-repeat-v", Name: "Library Panel with Vertical Repeat", Type: "timeseries"}, + {UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"}, + } + + underlyingDS := newCountingDataSourceProvider(datasources) + underlyingLE := newCountingLibraryElementProvider(elements) + + cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute) + cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute) + + migration.ResetForTesting() + migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL) + + numDashboards := 5 + namespace := "default" + + for i := 0; i < numDashboards; i++ { + source := createTestV1Dashboard(namespace, "Dashboard "+string(rune('A'+i))) + target := &dashv2alpha1.Dashboard{} + + err := Convert_V1beta1_to_V2alpha1(source, target, nil, cachedDS, cachedLE) + require.NoError(t, err, "conversion %d should succeed", i) + require.NotNil(t, target.Spec) + } + + assert.Equal(t, int64(1), underlyingDS.getCallCount(), + "datasource provider should be called only once for %d conversions in same namespace", numDashboards) + assert.Equal(t, int64(1), underlyingLE.getCallCount(), + "library element provider should be called only once for %d conversions in same namespace", numDashboards) +} + +// TestConversionCaching_V1beta1_to_V2beta1 verifies caching works when converting V1beta1 to V2beta1 +func TestConversionCaching_V1beta1_to_V2beta1(t *testing.T) { + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + elements := []schemaversion.LibraryElementInfo{ + {UID: "lib-panel-repeat-v", Name: "Library Panel with Vertical Repeat", Type: "timeseries"}, + {UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"}, + } + + underlyingDS := newCountingDataSourceProvider(datasources) + underlyingLE := newCountingLibraryElementProvider(elements) + + cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute) + cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute) + + migration.ResetForTesting() + migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL) + + numDashboards := 5 + namespace := "default" + + for i := 0; i < numDashboards; i++ { + source := createTestV1Dashboard(namespace, "Dashboard "+string(rune('A'+i))) + target := &dashv2beta1.Dashboard{} + + err := Convert_V1beta1_to_V2beta1(source, target, nil, cachedDS, cachedLE) + require.NoError(t, err, "conversion %d should succeed", i) + require.NotNil(t, target.Spec) + } + + assert.Equal(t, int64(1), underlyingDS.getCallCount(), + "datasource provider should be called only once for %d conversions in same namespace", numDashboards) + assert.Equal(t, int64(1), underlyingLE.getCallCount(), + "library element provider should be called only once for %d conversions in same namespace", numDashboards) +} + +// TestConversionCaching_MultipleNamespaces verifies that different namespaces get separate cache entries +func TestConversionCaching_MultipleNamespaces(t *testing.T) { + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + elements := []schemaversion.LibraryElementInfo{ + {UID: "lib-panel-repeat-h", Name: "Library Panel with Horizontal Repeat", Type: "timeseries"}, + {UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"}, + } + + underlyingDS := newCountingDataSourceProvider(datasources) + underlyingLE := newCountingLibraryElementProvider(elements) + + cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, time.Minute) + cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, time.Minute) + + migration.ResetForTesting() + migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL) + + namespaces := []string{"default", "org-2", "org-3"} + numDashboardsPerNs := 3 + + for _, ns := range namespaces { + for i := 0; i < numDashboardsPerNs; i++ { + source := createTestV0Dashboard(ns, "Dashboard "+string(rune('A'+i))) + target := &dashv2alpha1.Dashboard{} + + err := Convert_V0_to_V2alpha1(source, target, nil, cachedDS, cachedLE) + require.NoError(t, err, "conversion for namespace %s should succeed", ns) + } + } + + // With caching, each namespace should result in one call to the underlying provider + expectedCalls := int64(len(namespaces)) + assert.Equal(t, expectedCalls, underlyingDS.getCallCount(), + "datasource provider should be called once per namespace (%d namespaces)", len(namespaces)) + assert.Equal(t, expectedCalls, underlyingLE.getCallCount(), + "library element provider should be called once per namespace (%d namespaces)", len(namespaces)) +} + +// TestConversionCaching_CacheDisabled verifies that TTL=0 disables caching +func TestConversionCaching_CacheDisabled(t *testing.T) { + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + elements := []schemaversion.LibraryElementInfo{ + {UID: "lib-panel-repeat-h", Name: "Library Panel with Horizontal Repeat", Type: "timeseries"}, + {UID: "lib-panel-no-repeat", Name: "Library Panel without Repeat", Type: "graph"}, + } + + underlyingDS := newCountingDataSourceProvider(datasources) + underlyingLE := newCountingLibraryElementProvider(elements) + + // TTL of 0 should disable caching - the wrapper returns the underlying provider directly + cachedDS := schemaversion.WrapIndexProviderWithCache(underlyingDS, 0) + cachedLE := schemaversion.WrapLibraryElementProviderWithCache(underlyingLE, 0) + + migration.ResetForTesting() + migration.Initialize(cachedDS, cachedLE, migration.DefaultCacheTTL) + + numDashboards := 3 + namespace := "default" + + for i := 0; i < numDashboards; i++ { + source := createTestV0Dashboard(namespace, "Dashboard "+string(rune('A'+i))) + target := &dashv2alpha1.Dashboard{} + + err := Convert_V0_to_V2alpha1(source, target, nil, cachedDS, cachedLE) + require.NoError(t, err, "conversion %d should succeed", i) + } + + // Without caching, each conversion calls the underlying provider multiple times + // (once for each datasource lookup needed - variables and panels) + // The key check is that the count is GREATER than 1 per conversion (no caching benefit) + assert.Greater(t, underlyingDS.getCallCount(), int64(numDashboards), + "with cache disabled, conversions should call datasource provider multiple times") + // Library element provider is also called for each conversion without caching + assert.GreaterOrEqual(t, underlyingLE.getCallCount(), int64(numDashboards), + "with cache disabled, conversions should call library element provider multiple times") +} diff --git a/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go index 296a95ac054..ec7983b703b 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go @@ -488,7 +488,7 @@ func withConversionDataLossDetection(sourceFuncName, targetFuncName string, conv // Detect if data was lost if dataLossErr := detectConversionDataLoss(sourceStats, targetStats, sourceFuncName, targetFuncName); dataLossErr != nil { - logger.Error("Dashboard conversion data loss detected", + getLogger().Error("Dashboard conversion data loss detected", "sourceFunc", sourceFuncName, "targetFunc", targetFuncName, "sourcePanels", sourceStats.panelCount, @@ -504,7 +504,7 @@ func withConversionDataLossDetection(sourceFuncName, targetFuncName string, conv return dataLossErr } - logger.Debug("Dashboard conversion completed without data loss", + getLogger().Debug("Dashboard conversion completed without data loss", "sourceFunc", sourceFuncName, "targetFunc", targetFuncName, "panels", targetStats.panelCount, diff --git a/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection_test.go b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection_test.go index f49305996f9..816fd871029 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection_test.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection_test.go @@ -829,7 +829,7 @@ func TestDataLossDetectionOnAllInputFiles(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := testutil.NewDataSourceProvider(testutil.StandardTestConfig) leProvider := testutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() diff --git a/apps/dashboard/pkg/migration/conversion/conversion_test.go b/apps/dashboard/pkg/migration/conversion/conversion_test.go index 88dfb683225..63338e7ea70 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_test.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_test.go @@ -35,7 +35,7 @@ func TestConversionMatrixExist(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) versions := []metav1.Object{ &dashv0.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV0"}}}, @@ -89,7 +89,7 @@ func TestDashboardConversionToAllVersions(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() @@ -309,7 +309,7 @@ func TestMigratedDashboardsConversion(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() @@ -428,7 +428,7 @@ func setupTestConversionScheme(t *testing.T) *runtime.Scheme { t.Helper() dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) scheme := runtime.NewScheme() err := RegisterConversions(scheme, dsProvider, leProvider) @@ -527,7 +527,7 @@ func TestConversionMetrics(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -694,7 +694,7 @@ func TestConversionMetricsWrapper(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -864,7 +864,7 @@ func TestSchemaVersionExtraction(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -910,7 +910,7 @@ func TestConversionLogging(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -1003,7 +1003,7 @@ func TestConversionLogLevels(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) t.Run("log levels and structured fields verification", func(t *testing.T) { // Create test wrapper to verify logging behavior @@ -1076,7 +1076,7 @@ func TestConversionLoggingFields(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) // Use TestLibraryElementProvider for tests that need library panel models with repeat options leProvider := migrationtestutil.NewTestLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) t.Run("verify all log fields are present", func(t *testing.T) { // Test that the conversion wrapper includes all expected structured fields diff --git a/apps/dashboard/pkg/migration/conversion/metrics.go b/apps/dashboard/pkg/migration/conversion/metrics.go index 5b2700bce60..5a60aa848de 100644 --- a/apps/dashboard/pkg/migration/conversion/metrics.go +++ b/apps/dashboard/pkg/migration/conversion/metrics.go @@ -17,7 +17,9 @@ import ( "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) -var logger = logging.DefaultLogger.With("logger", "dashboard.conversion") +func getLogger() logging.Logger { + return logging.DefaultLogger.With("logger", "dashboard.conversion") +} // getErroredSchemaVersionFunc determines the schema version function that errored func getErroredSchemaVersionFunc(err error) string { @@ -197,9 +199,9 @@ func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversion ) if errorType == "schema_minimum_version_error" { - logger.Warn("Dashboard conversion failed", logFields...) + getLogger().Warn("Dashboard conversion failed", logFields...) } else { - logger.Error("Dashboard conversion failed", logFields...) + getLogger().Error("Dashboard conversion failed", logFields...) } } else { // Record success metrics @@ -235,7 +237,7 @@ func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversion ) } - logger.Debug("Dashboard conversion succeeded", successLogFields...) + getLogger().Debug("Dashboard conversion succeeded", successLogFields...) } return nil diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json index 863d7b2a102..7c7c479199d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.json @@ -76,9 +76,9 @@ "barGlow": false, "centerGlow": false, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -155,9 +155,9 @@ "barGlow": false, "centerGlow": true, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -234,9 +234,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -313,9 +313,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -392,9 +392,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -471,9 +471,9 @@ "barGlow": true, "centerGlow": true, "rounded": false, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -550,9 +550,9 @@ "barGlow": true, "centerGlow": true, "rounded": false, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -642,9 +642,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -721,9 +721,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -800,9 +800,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -879,9 +879,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -975,9 +975,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1054,9 +1054,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1133,9 +1133,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1212,9 +1212,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1291,9 +1291,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1387,9 +1387,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1470,9 +1470,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1553,9 +1553,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1645,10 +1645,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1731,10 +1731,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1831,10 +1831,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1919,10 +1919,10 @@ "centerGlow": true, "rounded": true, "sparkline": false, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2005,10 +2005,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2091,10 +2091,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2147,4 +2147,4 @@ "title": "Panel tests - Gauge (new)", "uid": "panel-tests-gauge-new", "weekStart": "" -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json index d4dd7980100..a3de6df336a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.json @@ -956,9 +956,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1162,4 +1162,4 @@ "title": "Panel tests - Old gauge to new", "uid": "panel-tests-old-gauge-to-new", "weekStart": "" -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json index ae1ef7cd04e..d6010e5100e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json @@ -42,7 +42,7 @@ "regex": "", "skipUrlSync": false, "refresh": 1 - }, + }, { "name": "query_var", "type": "query", @@ -81,6 +81,7 @@ "allValue": ".*", "multi": true, "regex": "/.*9090.*/", + "regexApplyTo": "text", "skipUrlSync": false, "refresh": 2, "sort": 1, @@ -107,7 +108,7 @@ }, { "selected": false, - "text": "staging", + "text": "staging", "value": "staging" }, { @@ -335,6 +336,7 @@ "allValue": "*", "multi": true, "regex": "/host[0-9]+/", + "regexApplyTo": "value", "skipUrlSync": false, "refresh": 1, "sort": 2, @@ -354,4 +356,4 @@ }, "links": [] } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json index 3084bbd32ea..e24f3826810 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v1beta1.json @@ -82,9 +82,9 @@ "barGlow": false, "centerGlow": false, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -161,9 +161,9 @@ "barGlow": false, "centerGlow": true, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -240,9 +240,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -319,9 +319,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -398,9 +398,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -477,9 +477,9 @@ "barGlow": true, "centerGlow": true, "rounded": false, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -556,9 +556,9 @@ "barGlow": true, "centerGlow": true, "rounded": false, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -648,9 +648,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -727,9 +727,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -806,9 +806,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -885,9 +885,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -981,9 +981,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1060,9 +1060,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1139,9 +1139,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1218,9 +1218,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1297,9 +1297,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1393,9 +1393,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1476,9 +1476,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1559,9 +1559,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1651,10 +1651,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1737,10 +1737,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1837,10 +1837,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1925,10 +1925,10 @@ "centerGlow": true, "rounded": true, "sparkline": false, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2011,10 +2011,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2097,10 +2097,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2160,4 +2160,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json index 5b5c3951ad8..893cb3a5d72 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2alpha1.json @@ -78,9 +78,9 @@ "barGlow": false, "centerGlow": false, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -171,10 +171,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -268,10 +268,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -367,10 +367,10 @@ "centerGlow": true, "rounded": true, "sparkline": false, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -465,10 +465,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -562,10 +562,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -658,9 +658,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -750,9 +750,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -842,9 +842,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -934,9 +934,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1026,9 +1026,9 @@ "barGlow": true, "centerGlow": true, "rounded": false, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1118,9 +1118,9 @@ "barGlow": true, "centerGlow": true, "rounded": false, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1206,9 +1206,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1298,9 +1298,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1390,9 +1390,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1482,9 +1482,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1578,9 +1578,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1666,9 +1666,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1758,9 +1758,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1854,9 +1854,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1950,9 +1950,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2050,9 +2050,9 @@ "barGlow": false, "centerGlow": true, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2142,9 +2142,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2234,9 +2234,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2327,10 +2327,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2821,4 +2821,4 @@ "variables": [] }, "status": {} -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json index 24125f197c5..ce8327e9434 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_new.v42.v2beta1.json @@ -82,9 +82,9 @@ "barGlow": false, "centerGlow": false, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -178,10 +178,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -278,10 +278,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -380,10 +380,10 @@ "centerGlow": true, "rounded": true, "sparkline": false, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -481,10 +481,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -581,10 +581,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -680,9 +680,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -775,9 +775,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -870,9 +870,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -965,9 +965,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1060,9 +1060,9 @@ "barGlow": true, "centerGlow": true, "rounded": false, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1155,9 +1155,9 @@ "barGlow": true, "centerGlow": true, "rounded": false, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1246,9 +1246,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1341,9 +1341,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1436,9 +1436,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1531,9 +1531,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1630,9 +1630,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1721,9 +1721,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1816,9 +1816,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1915,9 +1915,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2014,9 +2014,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2117,9 +2117,9 @@ "barGlow": false, "centerGlow": true, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2212,9 +2212,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2307,9 +2307,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2403,10 +2403,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2897,4 +2897,4 @@ "variables": [] }, "status": {} -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json index ddad9707104..0487d6480e8 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v1beta1.json @@ -962,9 +962,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1175,4 +1175,4 @@ "storedVersion": "v0alpha1" } } -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json index b485fbf84ed..b3841bd8c19 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2alpha1.json @@ -865,9 +865,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1615,4 +1615,4 @@ "variables": [] }, "status": {} -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json index 304058565f1..fdcda577f2e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/migrated_dev_dashboards/panel-gauge/v0alpha1.gauge_tests_old_to_new.v42.v2beta1.json @@ -902,9 +902,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1667,4 +1667,4 @@ "variables": [] }, "status": {} -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json index 4e12e6982ef..725e658dfc2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json @@ -94,6 +94,7 @@ "query": "label_values(up, instance)", "refresh": 2, "regex": "/.*9090.*/", + "regexApplyTo": "text", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", @@ -362,6 +363,7 @@ }, "refresh": 1, "regex": "/host[0-9]+/", + "regexApplyTo": "value", "skipUrlSync": false, "sort": 2, "tagValuesQuery": "", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json index c7c6c646a94..c51582691b9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json @@ -110,6 +110,7 @@ } }, "regex": "/.*9090.*/", + "regexApplyTo": "text", "sort": "alphabeticalAsc", "definition": "label_values(up, instance)", "options": [ @@ -401,6 +402,7 @@ } }, "regex": "/host[0-9]+/", + "regexApplyTo": "value", "sort": "alphabeticalDesc", "definition": "terms field:@host size:100", "options": [], diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json index 7b1a899d7fa..441258438a9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json @@ -111,6 +111,7 @@ } }, "regex": "/.*9090.*/", + "regexApplyTo": "text", "sort": "alphabeticalAsc", "definition": "label_values(up, instance)", "options": [ @@ -404,6 +405,7 @@ } }, "regex": "/host[0-9]+/", + "regexApplyTo": "value", "sort": "alphabeticalDesc", "definition": "terms field:@host size:100", "options": [], diff --git a/apps/dashboard/pkg/migration/conversion/v0_test.go b/apps/dashboard/pkg/migration/conversion/v0_test.go index ca2210f01e8..5566a72c767 100644 --- a/apps/dashboard/pkg/migration/conversion/v0_test.go +++ b/apps/dashboard/pkg/migration/conversion/v0_test.go @@ -20,7 +20,7 @@ func TestV0ConversionErrorHandling(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) tests := []struct { name string @@ -132,7 +132,7 @@ func TestV0ConversionErrorPropagation(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) t.Run("ConvertDashboard_V0_to_V1beta1 returns error on migration failure", func(t *testing.T) { source := &dashv0.Dashboard{ @@ -206,7 +206,7 @@ func TestV0ConversionSuccessPaths(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) t.Run("Convert_V0_to_V1beta1 success path returns nil", func(t *testing.T) { source := &dashv0.Dashboard{ @@ -275,7 +275,7 @@ func TestV0ConversionSecondStepErrors(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) t.Run("Convert_V0_to_V2alpha1 sets status on first step error", func(t *testing.T) { // Create a dashboard that will fail v0->v1beta1 conversion diff --git a/apps/dashboard/pkg/migration/conversion/v1_test.go b/apps/dashboard/pkg/migration/conversion/v1_test.go index 4525e974e01..28cf6e49a3b 100644 --- a/apps/dashboard/pkg/migration/conversion/v1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v1_test.go @@ -19,7 +19,7 @@ func TestV1ConversionErrorHandling(t *testing.T) { // Initialize the migrator with a test data source provider dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) t.Run("Convert_V1beta1_to_V2alpha1 sets status on conversion error", func(t *testing.T) { // Create a dashboard that will cause conversion to fail diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index 236a4337efb..4d6fd791fa9 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -229,6 +229,16 @@ func getBoolField(m map[string]interface{}, key string, defaultValue bool) bool return defaultValue } +func getUnionField[T ~string](m map[string]interface{}, key string) *T { + if val, ok := m[key]; ok { + if str, ok := val.(string); ok && str != "" { + result := T(str) + return &result + } + } + return nil +} + // Helper function to create int64 pointer func int64Ptr(i int64) *int64 { return &i @@ -1195,6 +1205,7 @@ func buildQueryVariable(ctx context.Context, varMap map[string]interface{}, comm Refresh: transformVariableRefreshToEnum(varMap["refresh"]), Sort: transformVariableSortToEnum(varMap["sort"]), Regex: schemaversion.GetStringValue(varMap, "regex"), + RegexApplyTo: getUnionField[dashv2alpha1.DashboardVariableRegexApplyTo](varMap, "regexApplyTo"), Query: buildDataQueryKindForVariable(varMap["query"], datasourceType), AllowCustomValue: getBoolField(varMap, "allowCustomValue", true), }, diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go index 3dad9188fe7..a80bf9c8072 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go @@ -19,7 +19,7 @@ func TestV1beta1ToV2alpha1(t *testing.T) { // Initialize the migrator with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() diff --git a/apps/dashboard/pkg/migration/conversion/v2.go b/apps/dashboard/pkg/migration/conversion/v2.go index fee798d3ec5..fa8a49e91b4 100644 --- a/apps/dashboard/pkg/migration/conversion/v2.go +++ b/apps/dashboard/pkg/migration/conversion/v2.go @@ -11,10 +11,10 @@ import ( "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) -func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { // Convert v2alpha1 → v1beta1 first, then v1beta1 → v0 v1beta1 := &dashv1.Dashboard{} - if err := ConvertDashboard_V2alpha1_to_V1beta1(in, v1beta1, scope, dsIndexProvider); err != nil { + if err := ConvertDashboard_V2alpha1_to_V1beta1(in, v1beta1, scope); err != nil { out.ObjectMeta = in.ObjectMeta out.APIVersion = dashv0.APIVERSION out.Kind = in.Kind @@ -53,13 +53,13 @@ func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, s return nil } -func Convert_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func Convert_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta out.APIVersion = dashv1.APIVERSION out.Kind = in.Kind // Convert the spec - if err := ConvertDashboard_V2alpha1_to_V1beta1(in, out, scope, dsIndexProvider); err != nil { + if err := ConvertDashboard_V2alpha1_to_V1beta1(in, out, scope); err != nil { out.Status = dashv1.DashboardStatus{ Conversion: &dashv1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv2alpha1.VERSION), @@ -179,7 +179,7 @@ func Convert_V2beta1_to_V1beta1(in *dashv2beta1.Dashboard, out *dashv1.Dashboard // Convert v2alpha1 → v1beta1 // Note: ConvertDashboard_V2alpha1_to_V1beta1 will set out.ObjectMeta from v2alpha1, // but we've already set it from the original input, so it will be preserved - if err := ConvertDashboard_V2alpha1_to_V1beta1(v2alpha1, out, scope, dsIndexProvider); err != nil { + if err := ConvertDashboard_V2alpha1_to_V1beta1(v2alpha1, out, scope); err != nil { out.Status = dashv1.DashboardStatus{ Conversion: &dashv1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv2beta1.VERSION), diff --git a/apps/dashboard/pkg/migration/conversion/v2_test.go b/apps/dashboard/pkg/migration/conversion/v2_test.go index 18e0713fa84..d2010062223 100644 --- a/apps/dashboard/pkg/migration/conversion/v2_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2_test.go @@ -18,7 +18,7 @@ func TestV2alpha1ConversionErrorHandling(t *testing.T) { // Initialize the migrator with test data source and library element providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) t.Run("Convert_V2alpha1_to_V1beta1 sets status on conversion", func(t *testing.T) { // Create a dashboard for conversion @@ -39,7 +39,7 @@ func TestV2alpha1ConversionErrorHandling(t *testing.T) { } target := &dashv1.Dashboard{} - err := Convert_V2alpha1_to_V1beta1(source, target, nil, dsProvider) + err := Convert_V2alpha1_to_V1beta1(source, target, nil) // Convert_V2alpha1_to_V1beta1 doesn't return error, just sets status require.NoError(t, err, "Convert_V2alpha1_to_V1beta1 doesn't return error") @@ -90,7 +90,7 @@ func TestV2beta1ConversionErrorHandling(t *testing.T) { // Initialize the migrator with test data source and library element providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) t.Run("Convert_V2beta1_to_V1beta1 sets status on first step failure", func(t *testing.T) { // Create a dashboard that might cause conversion to fail on first step (v2beta1 -> v2alpha1) diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go index 180a8d603bb..fed2691db27 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1.go @@ -1,14 +1,12 @@ package conversion import ( - "context" "fmt" - "k8s.io/apimachinery/pkg/conversion" - dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + "k8s.io/apimachinery/pkg/conversion" ) // ConvertDashboard_V2alpha1_to_V1beta1 converts a v2alpha1 dashboard to v1beta1 format. @@ -16,19 +14,13 @@ import ( // that represents the v1 dashboard JSON format. // The dsIndexProvider is used to resolve default datasources when queries/variables/annotations // don't have explicit datasource references. -func ConvertDashboard_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error { +func ConvertDashboard_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta out.APIVersion = dashv1.APIVERSION out.Kind = in.Kind // Preserve the Kind from input (should be "Dashboard") - // Get datasource index for resolving default datasources - var dsIndex *schemaversion.DatasourceIndex - if dsIndexProvider != nil { - dsIndex = dsIndexProvider.Index(context.Background()) - } - // Convert the spec to v1beta1 unstructured format - dashboardJSON, err := convertDashboardSpec_V2alpha1_to_V1beta1(&in.Spec, dsIndex) + dashboardJSON, err := convertDashboardSpec_V2alpha1_to_V1beta1(&in.Spec) if err != nil { return fmt.Errorf("failed to convert dashboard spec: %w", err) } @@ -39,7 +31,7 @@ func ConvertDashboard_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv return nil } -func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) { +func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec) (map[string]interface{}, error) { dashboard := make(map[string]interface{}) // Convert basic fields @@ -75,7 +67,7 @@ func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec, ds } // Convert panels from elements and layout - panels, err := convertPanelsFromElementsAndLayout(in.Elements, in.Layout, dsIndex) + panels, err := convertPanelsFromElementsAndLayout(in.Elements, in.Layout) if err != nil { return nil, fmt.Errorf("failed to convert panels: %w", err) } @@ -90,7 +82,7 @@ func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec, ds } // Convert variables - variables := convertVariablesToV1(in.Variables, dsIndex) + variables := convertVariablesToV1(in.Variables) if len(variables) > 0 { dashboard["templating"] = map[string]interface{}{ "list": variables, @@ -98,7 +90,7 @@ func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec, ds } // Convert annotations - always include even if empty to prevent DashboardModel from adding built-in - annotations := convertAnnotationsToV1(in.Annotations, dsIndex) + annotations := convertAnnotationsToV1(in.Annotations) dashboard["annotations"] = map[string]interface{}{ "list": annotations, } @@ -236,28 +228,28 @@ func countTotalPanels(panels []interface{}) int { // - RowsLayout: Rows become row panels; nested structures are flattened // - AutoGridLayout: Calculates gridPos based on column count and row height // - TabsLayout: Tabs become expanded row panels; content is flattened -func convertPanelsFromElementsAndLayout(elements map[string]dashv2alpha1.DashboardElement, layout dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) { +func convertPanelsFromElementsAndLayout(elements map[string]dashv2alpha1.DashboardElement, layout dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind) ([]interface{}, error) { if layout.GridLayoutKind != nil { - return convertGridLayoutToPanels(elements, layout.GridLayoutKind, dsIndex) + return convertGridLayoutToPanels(elements, layout.GridLayoutKind) } if layout.RowsLayoutKind != nil { - return convertRowsLayoutToPanels(elements, layout.RowsLayoutKind, dsIndex) + return convertRowsLayoutToPanels(elements, layout.RowsLayoutKind) } if layout.AutoGridLayoutKind != nil { - return convertAutoGridLayoutToPanels(elements, layout.AutoGridLayoutKind, dsIndex) + return convertAutoGridLayoutToPanels(elements, layout.AutoGridLayoutKind) } if layout.TabsLayoutKind != nil { - return convertTabsLayoutToPanels(elements, layout.TabsLayoutKind, dsIndex) + return convertTabsLayoutToPanels(elements, layout.TabsLayoutKind) } // No layout specified, return empty panels return []interface{}{}, nil } -func convertGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, gridLayout *dashv2alpha1.DashboardGridLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) { +func convertGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, gridLayout *dashv2alpha1.DashboardGridLayoutKind) ([]interface{}, error) { panels := make([]interface{}, 0, len(gridLayout.Spec.Items)) for _, item := range gridLayout.Spec.Items { @@ -266,7 +258,7 @@ func convertGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement return nil, fmt.Errorf("panel with uid %s not found in the dashboard elements", item.Spec.Element.Name) } - panel, err := convertPanelFromElement(&element, &item, dsIndex) + panel, err := convertPanelFromElement(&element, &item) if err != nil { return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } @@ -279,21 +271,21 @@ func convertGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement // convertRowsLayoutToPanels converts a RowsLayout to V1 panels. // All nested structures (rows within rows, tabs within rows) are flattened to the root level. // Each row becomes a row panel, and nested content is added sequentially after it. -func convertRowsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) { - return convertNestedLayoutToPanels(elements, rowsLayout, nil, dsIndex, 0) +func convertRowsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind) ([]interface{}, error) { + return convertNestedLayoutToPanels(elements, rowsLayout, nil, 0) } // convertNestedLayoutToPanels handles arbitrary nesting of RowsLayout and TabsLayout. // It processes each row/tab in order, tracking Y position to ensure panels don't overlap. // The function recursively flattens nested structures to produce a flat V1 panel array. -func convertNestedLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind, dsIndex *schemaversion.DatasourceIndex, yOffset int64) ([]interface{}, error) { +func convertNestedLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind, yOffset int64) ([]interface{}, error) { panels := make([]interface{}, 0) currentY := yOffset // Process RowsLayout if rowsLayout != nil { for _, row := range rowsLayout.Spec.Rows { - rowPanels, newY, err := processRowItem(elements, &row, dsIndex, currentY) + rowPanels, newY, err := processRowItem(elements, &row, currentY) if err != nil { return nil, err } @@ -305,7 +297,7 @@ func convertNestedLayoutToPanels(elements map[string]dashv2alpha1.DashboardEleme // Process TabsLayout (tabs are converted to rows) if tabsLayout != nil { for _, tab := range tabsLayout.Spec.Tabs { - tabPanels, newY, err := processTabItem(elements, &tab, dsIndex, currentY) + tabPanels, newY, err := processTabItem(elements, &tab, currentY) if err != nil { return nil, err } @@ -324,7 +316,7 @@ func convertNestedLayoutToPanels(elements map[string]dashv2alpha1.DashboardEleme // - Collapsed row: Panels stored inside row.panels with absolute Y positions // - Expanded row: Panels added to top level after the row panel // - Nested layouts: Parent row is preserved; nested content is flattened after it -func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dashv2alpha1.DashboardRowsLayoutRowKind, dsIndex *schemaversion.DatasourceIndex, startY int64) ([]interface{}, int64, error) { +func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dashv2alpha1.DashboardRowsLayoutRowKind, startY int64) ([]interface{}, int64, error) { panels := make([]interface{}, 0) currentY := startY @@ -354,7 +346,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash } // Then process nested rows - nestedPanels, err := convertNestedLayoutToPanels(elements, row.Spec.Layout.RowsLayoutKind, nil, dsIndex, currentY) + nestedPanels, err := convertNestedLayoutToPanels(elements, row.Spec.Layout.RowsLayoutKind, nil, currentY) if err != nil { return nil, 0, err } @@ -387,7 +379,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash } // Then process nested tabs - nestedPanels, err := convertNestedLayoutToPanels(elements, nil, row.Spec.Layout.TabsLayoutKind, dsIndex, currentY) + nestedPanels, err := convertNestedLayoutToPanels(elements, nil, row.Spec.Layout.TabsLayoutKind, currentY) if err != nil { return nil, 0, err } @@ -429,7 +421,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash // Add collapsed panels if row is collapsed (panels use absolute Y positions) if isCollapsed { - collapsedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, dsIndex, currentY+1) + collapsedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, currentY+1) if err != nil { return nil, 0, err } @@ -444,7 +436,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash // Add panels from row layout (only for expanded rows or hidden header rows) if !isCollapsed || isHiddenHeader { - rowPanels, newY, err := extractExpandedPanels(elements, &row.Spec.Layout, dsIndex, currentY, isHiddenHeader, startY) + rowPanels, newY, err := extractExpandedPanels(elements, &row.Spec.Layout, currentY, isHiddenHeader, startY) if err != nil { return nil, 0, err } @@ -459,7 +451,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash // Each tab becomes an expanded row panel (collapsed=false) with an empty panels array. // The tab's content is flattened and added to the top level after the row panel. // Nested layouts within the tab are recursively processed. -func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dashv2alpha1.DashboardTabsLayoutTabKind, dsIndex *schemaversion.DatasourceIndex, startY int64) ([]interface{}, int64, error) { +func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dashv2alpha1.DashboardTabsLayoutTabKind, startY int64) ([]interface{}, int64, error) { panels := make([]interface{}, 0) currentY := startY @@ -487,7 +479,7 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash // Handle nested layouts inside the tab if tab.Spec.Layout.RowsLayoutKind != nil { // Nested RowsLayout inside tab - nestedPanels, err := convertNestedLayoutToPanels(elements, tab.Spec.Layout.RowsLayoutKind, nil, dsIndex, currentY) + nestedPanels, err := convertNestedLayoutToPanels(elements, tab.Spec.Layout.RowsLayoutKind, nil, currentY) if err != nil { return nil, 0, err } @@ -495,7 +487,7 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash currentY = getMaxYFromPanels(nestedPanels, currentY) } else if tab.Spec.Layout.TabsLayoutKind != nil { // Nested TabsLayout inside tab - nestedPanels, err := convertNestedLayoutToPanels(elements, nil, tab.Spec.Layout.TabsLayoutKind, dsIndex, currentY) + nestedPanels, err := convertNestedLayoutToPanels(elements, nil, tab.Spec.Layout.TabsLayoutKind, currentY) if err != nil { return nil, 0, err } @@ -512,7 +504,7 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash adjustedItem := item adjustedItem.Spec.Y = item.Spec.Y + currentY - panel, err := convertPanelFromElement(&element, &adjustedItem, dsIndex) + panel, err := convertPanelFromElement(&element, &adjustedItem) if err != nil { return nil, 0, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } @@ -525,7 +517,7 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash } } else if tab.Spec.Layout.AutoGridLayoutKind != nil { // AutoGridLayout inside tab - convert with Y offset - autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, tab.Spec.Layout.AutoGridLayoutKind, dsIndex, currentY) + autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, tab.Spec.Layout.AutoGridLayoutKind, currentY) if err != nil { return nil, 0, err } @@ -540,7 +532,7 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash // Panels are positioned with absolute Y coordinates (baseY + relative Y). // This matches V1 behavior where collapsed row panels store their children // with Y positions as if the row were expanded at that location. -func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, dsIndex *schemaversion.DatasourceIndex, baseY int64) ([]interface{}, error) { +func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, baseY int64) ([]interface{}, error) { panels := make([]interface{}, 0) if layout.GridLayoutKind != nil { @@ -552,7 +544,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo // Create a copy with adjusted Y position adjustedItem := item adjustedItem.Spec.Y = item.Spec.Y + baseY - panel, err := convertPanelFromElement(&element, &adjustedItem, dsIndex) + panel, err := convertPanelFromElement(&element, &adjustedItem) if err != nil { return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } @@ -561,7 +553,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo } // Handle AutoGridLayout for collapsed rows with Y offset if layout.AutoGridLayoutKind != nil { - autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, dsIndex, baseY) + autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, baseY) if err != nil { return nil, err } @@ -571,7 +563,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo if layout.RowsLayoutKind != nil { currentY := baseY for _, row := range layout.RowsLayoutKind.Spec.Rows { - nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, dsIndex, currentY) + nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, currentY) if err != nil { return nil, err } @@ -582,7 +574,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo if layout.TabsLayoutKind != nil { currentY := baseY for _, tab := range layout.TabsLayoutKind.Spec.Tabs { - nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, dsIndex, currentY) + nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, currentY) if err != nil { return nil, err } @@ -596,7 +588,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo // extractCollapsedPanelsFromTabLayoutWithAbsoluteY extracts panels from a tab layout with absolute Y. // Similar to extractCollapsedPanelsWithAbsoluteY but handles the tab-specific layout type. -func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, dsIndex *schemaversion.DatasourceIndex, baseY int64) ([]interface{}, error) { +func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, baseY int64) ([]interface{}, error) { panels := make([]interface{}, 0) if layout.GridLayoutKind != nil { @@ -607,7 +599,7 @@ func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2 } adjustedItem := item adjustedItem.Spec.Y = item.Spec.Y + baseY - panel, err := convertPanelFromElement(&element, &adjustedItem, dsIndex) + panel, err := convertPanelFromElement(&element, &adjustedItem) if err != nil { return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } @@ -615,7 +607,7 @@ func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2 } } if layout.AutoGridLayoutKind != nil { - autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, dsIndex, baseY) + autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, baseY) if err != nil { return nil, err } @@ -624,7 +616,7 @@ func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2 if layout.RowsLayoutKind != nil { currentY := baseY for _, row := range layout.RowsLayoutKind.Spec.Rows { - nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, dsIndex, currentY) + nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, currentY) if err != nil { return nil, err } @@ -635,7 +627,7 @@ func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2 if layout.TabsLayoutKind != nil { currentY := baseY for _, tab := range layout.TabsLayoutKind.Spec.Tabs { - nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, dsIndex, currentY) + nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, currentY) if err != nil { return nil, err } @@ -679,7 +671,7 @@ func getLayoutHeightFromTab(layout *dashv2alpha1.DashboardGridLayoutKindOrRowsLa // - Explicit row: Add (currentY - 1) to relative Y for absolute positioning // // Returns the panels and the new Y position for the next row. -func extractExpandedPanels(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, dsIndex *schemaversion.DatasourceIndex, currentY int64, isHiddenHeader bool, startY int64) ([]interface{}, int64, error) { +func extractExpandedPanels(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, currentY int64, isHiddenHeader bool, startY int64) ([]interface{}, int64, error) { panels := make([]interface{}, 0) // For hidden headers, don't track Y changes (matches original behavior) maxY := startY @@ -700,7 +692,7 @@ func extractExpandedPanels(elements map[string]dashv2alpha1.DashboardElement, la } // For hidden headers: don't adjust Y, keep item.Spec.Y as-is - panel, err := convertPanelFromElement(&element, &adjustedItem, dsIndex) + panel, err := convertPanelFromElement(&element, &adjustedItem) if err != nil { return nil, 0, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } @@ -725,7 +717,7 @@ func extractExpandedPanels(elements map[string]dashv2alpha1.DashboardElement, la yOffset = currentY - 1 } - autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, dsIndex, yOffset) + autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, yOffset) if err != nil { return nil, 0, err } @@ -788,7 +780,7 @@ func getLayoutHeight(layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayou // convertAutoGridLayoutToPanelsWithOffset converts AutoGridLayout with a Y offset. // Same as convertAutoGridLayoutToPanels but starts at yOffset instead of 0. // Used when AutoGridLayout appears inside rows or tabs. -func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind, dsIndex *schemaversion.DatasourceIndex, yOffset int64) ([]interface{}, error) { +func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind, yOffset int64) ([]interface{}, error) { panels := make([]interface{}, 0, len(autoGridLayout.Spec.Items)) const ( @@ -850,7 +842,7 @@ func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.Da }, } - panel, err := convertPanelFromElement(&element, &gridItem, dsIndex) + panel, err := convertPanelFromElement(&element, &gridItem) if err != nil { return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } @@ -876,7 +868,7 @@ func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.Da // // Width: 24 / maxColumnCount (default 3 columns = 8 units wide) // Height: Predefined grid units per mode (see pixelsToGridUnits for custom) -func convertAutoGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) { +func convertAutoGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind) ([]interface{}, error) { panels := make([]interface{}, 0, len(autoGridLayout.Spec.Items)) const ( @@ -963,7 +955,7 @@ func convertAutoGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardEle } } - panel, err := convertPanelFromElement(&element, &gridItem, dsIndex) + panel, err := convertPanelFromElement(&element, &gridItem) if err != nil { return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err) } @@ -984,11 +976,11 @@ func convertAutoGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardEle // V1 has no native tab concept, so tabs are converted to expanded row panels. // Each tab becomes a row panel (collapsed=false, panels=[]) with its content // flattened to the top level. Tab order is preserved in the output. -func convertTabsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) { - return convertNestedLayoutToPanels(elements, nil, tabsLayout, dsIndex, 0) +func convertTabsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind) ([]interface{}, error) { + return convertNestedLayoutToPanels(elements, nil, tabsLayout, 0) } -func convertPanelFromElement(element *dashv2alpha1.DashboardElement, layoutItem *dashv2alpha1.DashboardGridLayoutItemKind, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) { +func convertPanelFromElement(element *dashv2alpha1.DashboardElement, layoutItem *dashv2alpha1.DashboardGridLayoutItemKind) (map[string]interface{}, error) { panel := make(map[string]interface{}) // Set grid position @@ -1017,7 +1009,7 @@ func convertPanelFromElement(element *dashv2alpha1.DashboardElement, layoutItem } if element.PanelKind != nil { - return convertPanelKindToV1(element.PanelKind, panel, dsIndex) + return convertPanelKindToV1(element.PanelKind, panel) } if element.LibraryPanelKind != nil { @@ -1027,7 +1019,7 @@ func convertPanelFromElement(element *dashv2alpha1.DashboardElement, layoutItem return nil, fmt.Errorf("element has neither PanelKind nor LibraryPanelKind") } -func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[string]interface{}, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) { +func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[string]interface{}) (map[string]interface{}, error) { spec := panelKind.Spec panel["id"] = int(spec.Id) @@ -1069,14 +1061,14 @@ func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[ // Convert queries (targets) targets := make([]map[string]interface{}, 0, len(spec.Data.Spec.Queries)) for _, query := range spec.Data.Spec.Queries { - target := convertPanelQueryToV1(&query, dsIndex) + target := convertPanelQueryToV1(&query) targets = append(targets, target) } panel["targets"] = targets // Detect mixed datasource - set panel.datasource to "mixed" if queries use different datasources // This matches the frontend behavior in getPanelDataSource (layoutSerializers/utils.ts) - if mixedDS := detectMixedDatasource(spec.Data.Spec.Queries, dsIndex); mixedDS != nil { + if mixedDS := detectMixedDatasource(spec.Data.Spec.Queries); mixedDS != nil { panel["datasource"] = mixedDS } @@ -1125,7 +1117,7 @@ func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[ return panel, nil } -func convertPanelQueryToV1(query *dashv2alpha1.DashboardPanelQueryKind, dsIndex *schemaversion.DatasourceIndex) map[string]interface{} { +func convertPanelQueryToV1(query *dashv2alpha1.DashboardPanelQueryKind) map[string]interface{} { target := make(map[string]interface{}) // Copy query spec (excluding refId, hide, datasource which are handled separately) @@ -1150,7 +1142,7 @@ func convertPanelQueryToV1(query *dashv2alpha1.DashboardPanelQueryKind, dsIndex } // Resolve datasource based on V2 input (reuse shared function) - datasource := getDataSourceForQuery(query.Spec.Datasource, query.Spec.Query.Kind, nil) + datasource := getDataSourceForQuery(query.Spec.Datasource, query.Spec.Query.Kind) if datasource != nil { target["datasource"] = datasource } @@ -1164,7 +1156,7 @@ func convertPanelQueryToV1(query *dashv2alpha1.DashboardPanelQueryKind, dsIndex // - Else if queryKind (type) is non-empty → return {type} only // - Else → return nil (no datasource) // Used for variables and annotations. Panel queries use convertPanelQueryToV1Target. -func getDataSourceForQuery(explicitDS *dashv2alpha1.DashboardDataSourceRef, queryKind string, _ *schemaversion.DatasourceIndex) map[string]interface{} { +func getDataSourceForQuery(explicitDS *dashv2alpha1.DashboardDataSourceRef, queryKind string) map[string]interface{} { // Case 1: Explicit datasource with UID provided if explicitDS != nil && explicitDS.Uid != nil && *explicitDS.Uid != "" { datasource := map[string]interface{}{ @@ -1195,7 +1187,7 @@ func getDataSourceForQuery(explicitDS *dashv2alpha1.DashboardDataSourceRef, quer // Compares based on V2 input without runtime resolution: // - If query has explicit datasource.uid → use that UID and type // - Else → use query.Kind as type (empty UID) -func detectMixedDatasource(queries []dashv2alpha1.DashboardPanelQueryKind, _ *schemaversion.DatasourceIndex) map[string]interface{} { +func detectMixedDatasource(queries []dashv2alpha1.DashboardPanelQueryKind) map[string]interface{} { if len(queries) == 0 { return nil } @@ -1254,7 +1246,7 @@ func convertLibraryPanelKindToV1(libPanelKind *dashv2alpha1.DashboardLibraryPane return panel, nil } -func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind, dsIndex *schemaversion.DatasourceIndex) []map[string]interface{} { +func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(variables)) for _, variable := range variables { @@ -1262,7 +1254,7 @@ func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind, dsInde var err error if variable.QueryVariableKind != nil { - varMap, err = convertQueryVariableToV1(variable.QueryVariableKind, dsIndex) + varMap, err = convertQueryVariableToV1(variable.QueryVariableKind) } else if variable.DatasourceVariableKind != nil { varMap, err = convertDatasourceVariableToV1(variable.DatasourceVariableKind) } else if variable.CustomVariableKind != nil { @@ -1274,9 +1266,9 @@ func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind, dsInde } else if variable.TextVariableKind != nil { varMap, err = convertTextVariableToV1(variable.TextVariableKind) } else if variable.GroupByVariableKind != nil { - varMap, err = convertGroupByVariableToV1(variable.GroupByVariableKind, dsIndex) + varMap, err = convertGroupByVariableToV1(variable.GroupByVariableKind) } else if variable.AdhocVariableKind != nil { - varMap, err = convertAdhocVariableToV1(variable.AdhocVariableKind, dsIndex) + varMap, err = convertAdhocVariableToV1(variable.AdhocVariableKind) } else if variable.SwitchVariableKind != nil { varMap, err = convertSwitchVariableToV1(variable.SwitchVariableKind) } @@ -1289,7 +1281,7 @@ func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind, dsInde return result } -func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) { +func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind) (map[string]interface{}, error) { spec := variable.Spec varMap := map[string]interface{}{ "name": spec.Name, @@ -1320,6 +1312,9 @@ func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind, if spec.Definition != nil { varMap["definition"] = *spec.Definition } + if spec.RegexApplyTo != nil { + varMap["regexApplyTo"] = string(*spec.RegexApplyTo) + } varMap["allowCustomValue"] = spec.AllowCustomValue // Convert query - handle LEGACY_STRING_VALUE_KEY @@ -1336,7 +1331,7 @@ func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind, } // Resolve datasource - use explicit datasource or resolve from query kind (datasource type)/default - datasource := getDataSourceForQuery(spec.Datasource, spec.Query.Kind, dsIndex) + datasource := getDataSourceForQuery(spec.Datasource, spec.Query.Kind) if datasource != nil { varMap["datasource"] = datasource } @@ -1486,7 +1481,7 @@ func convertTextVariableToV1(variable *dashv2alpha1.DashboardTextVariableKind) ( return varMap, nil } -func convertGroupByVariableToV1(variable *dashv2alpha1.DashboardGroupByVariableKind, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) { +func convertGroupByVariableToV1(variable *dashv2alpha1.DashboardGroupByVariableKind) (map[string]interface{}, error) { spec := variable.Spec varMap := map[string]interface{}{ "name": spec.Name, @@ -1509,7 +1504,7 @@ func convertGroupByVariableToV1(variable *dashv2alpha1.DashboardGroupByVariableK } // Resolve datasource - GroupBy variables don't have a query kind, so use empty string (will fall back to default) - datasource := getDataSourceForQuery(spec.Datasource, "", dsIndex) + datasource := getDataSourceForQuery(spec.Datasource, "") if datasource != nil { varMap["datasource"] = datasource } @@ -1517,7 +1512,7 @@ func convertGroupByVariableToV1(variable *dashv2alpha1.DashboardGroupByVariableK return varMap, nil } -func convertAdhocVariableToV1(variable *dashv2alpha1.DashboardAdhocVariableKind, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) { +func convertAdhocVariableToV1(variable *dashv2alpha1.DashboardAdhocVariableKind) (map[string]interface{}, error) { spec := variable.Spec varMap := map[string]interface{}{ "name": spec.Name, @@ -1536,7 +1531,7 @@ func convertAdhocVariableToV1(variable *dashv2alpha1.DashboardAdhocVariableKind, varMap["allowCustomValue"] = spec.AllowCustomValue // Resolve datasource - Adhoc variables don't have a query kind, so use empty string (will fall back to default) - datasource := getDataSourceForQuery(spec.Datasource, "", dsIndex) + datasource := getDataSourceForQuery(spec.Datasource, "") if datasource != nil { varMap["datasource"] = datasource } @@ -1663,7 +1658,7 @@ func convertSwitchVariableToV1(variable *dashv2alpha1.DashboardSwitchVariableKin return varMap, nil } -func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryKind, dsIndex *schemaversion.DatasourceIndex) []map[string]interface{} { +func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryKind) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(annotations)) for _, annotation := range annotations { @@ -1686,7 +1681,7 @@ func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryK if annotation.Spec.Query != nil { queryKind = annotation.Spec.Query.Kind } - datasource := getDataSourceForQuery(annotation.Spec.Datasource, queryKind, dsIndex) + datasource := getDataSourceForQuery(annotation.Spec.Datasource, queryKind) if datasource != nil { annotationMap["datasource"] = datasource } diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1_test.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1_test.go index 6ed9804fc68..b3b4fb98395 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1beta1_test.go @@ -282,7 +282,7 @@ func TestV2alpha1ToV1beta1LayoutErrors(t *testing.T) { // Initialize the migrator with test data source and library element providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() @@ -498,7 +498,7 @@ func TestV2alpha1ToV1beta1BasicFields(t *testing.T) { // Initialize the migrator with test data source and library element providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go index 452f0140047..5b3a0d115b8 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go @@ -767,6 +767,7 @@ func convertQueryVariableSpec_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardQuer out.SkipUrlSync = in.SkipUrlSync out.Description = in.Description out.Regex = in.Regex + out.RegexApplyTo = (*dashv2beta1.DashboardVariableRegexApplyTo)(in.RegexApplyTo) out.Sort = dashv2beta1.DashboardVariableSort(in.Sort) out.Definition = in.Definition out.Options = convertVariableOptions_V2alpha1_to_V2beta1(in.Options) diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go index 028197bd655..e92c9215c34 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go @@ -18,7 +18,7 @@ func TestV2alpha1ToV2beta1(t *testing.T) { // Initialize the migrator with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go index 95dfcf76c9d..aa7e8e5f36d 100644 --- a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go @@ -806,6 +806,7 @@ func convertQueryVariableSpec_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardQuery out.SkipUrlSync = in.SkipUrlSync out.Description = in.Description out.Regex = in.Regex + out.RegexApplyTo = (*dashv2alpha1.DashboardVariableRegexApplyTo)(in.RegexApplyTo) out.Sort = dashv2alpha1.DashboardVariableSort(in.Sort) out.Definition = in.Definition out.Options = convertVariableOptions_V2beta1_to_V2alpha1(in.Options) diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go index 23dcd36ee17..8b84ab40928 100644 --- a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go @@ -24,7 +24,7 @@ func TestV2beta1ToV2alpha1RoundTrip(t *testing.T) { // Initialize the migrator with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() @@ -107,7 +107,7 @@ func TestV2beta1ToV2alpha1FromOutputFiles(t *testing.T) { // Initialize the migrator with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() @@ -193,7 +193,7 @@ func TestV2beta1ToV2alpha1(t *testing.T) { // Initialize the migrator with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) + migration.Initialize(dsProvider, leProvider, migration.DefaultCacheTTL) // Set up conversion scheme scheme := runtime.NewScheme() diff --git a/apps/dashboard/pkg/migration/migrate.go b/apps/dashboard/pkg/migration/migrate.go index 9c87a45b2db..d1b0fcbb7af 100644 --- a/apps/dashboard/pkg/migration/migrate.go +++ b/apps/dashboard/pkg/migration/migrate.go @@ -4,13 +4,19 @@ import ( "context" "fmt" "sync" + "time" + "github.com/grafana/authlib/types" + "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) +// DefaultCacheTTL is the default TTL for the datasource and library element caches. +const DefaultCacheTTL = time.Minute + // Initialize provides the migrator singleton with required dependencies and builds the map of migrations. -func Initialize(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) { - migratorInstance.init(dsIndexProvider, leIndexProvider) +func Initialize(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider, cacheTTL time.Duration) { + migratorInstance.init(dsIndexProvider, leIndexProvider, cacheTTL) } // GetDataSourceIndexProvider returns the datasource index provider instance that was initialized. @@ -38,6 +44,34 @@ func ResetForTesting() { initOnce = sync.Once{} } +// PreloadCache preloads the datasource and library element caches for the given namespaces. +func PreloadCache(ctx context.Context, nsInfos []types.NamespaceInfo) { + // Wait for initialization to complete + <-migratorInstance.ready + + // Try to preload datasource cache + if preloadable, ok := migratorInstance.dsIndexProvider.(schemaversion.PreloadableCache); ok { + preloadable.Preload(ctx, nsInfos) + } + + // Try to preload library element cache + if preloadable, ok := migratorInstance.leIndexProvider.(schemaversion.PreloadableCache); ok { + preloadable.Preload(ctx, nsInfos) + } +} + +// PreloadCacheInBackground starts a goroutine that preloads the caches for the given namespaces. +func PreloadCacheInBackground(nsInfos []types.NamespaceInfo) { + go func() { + defer func() { + if r := recover(); r != nil { + logging.DefaultLogger.Error("panic during cache preloading", "error", r) + } + }() + PreloadCache(context.Background(), nsInfos) + }() +} + // Migrate migrates the given dashboard to the target version. // This will block until the migrator is initialized. func Migrate(ctx context.Context, dash map[string]interface{}, targetVersion int) error { @@ -59,11 +93,15 @@ type migrator struct { leIndexProvider schemaversion.LibraryElementIndexProvider } -func (m *migrator) init(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) { +func (m *migrator) init(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider, cacheTTL time.Duration) { initOnce.Do(func() { - m.dsIndexProvider = dsIndexProvider - m.leIndexProvider = leIndexProvider - m.migrations = schemaversion.GetMigrations(dsIndexProvider, leIndexProvider) + // Wrap the provider with org-aware TTL caching for all conversions. + // This prevents repeated DB queries across multiple conversion calls while allowing + // the cache to refresh periodically, making it suitable for long-lived singleton usage. + m.dsIndexProvider = schemaversion.WrapIndexProviderWithCache(dsIndexProvider, cacheTTL) + // Wrap library element provider with caching as well + m.leIndexProvider = schemaversion.WrapLibraryElementProviderWithCache(leIndexProvider, cacheTTL) + m.migrations = schemaversion.GetMigrations(m.dsIndexProvider, m.leIndexProvider) close(m.ready) }) } diff --git a/apps/dashboard/pkg/migration/migrate_test.go b/apps/dashboard/pkg/migration/migrate_test.go index 8bb150f91ca..4fe4ea90a60 100644 --- a/apps/dashboard/pkg/migration/migrate_test.go +++ b/apps/dashboard/pkg/migration/migrate_test.go @@ -10,10 +10,13 @@ import ( "path/filepath" "strconv" "strings" + "sync/atomic" "testing" "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/endpoints/request" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" migrationtestutil "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" @@ -31,7 +34,7 @@ func TestMigrate(t *testing.T) { ResetForTesting() dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - Initialize(dsProvider, leProvider) + Initialize(dsProvider, leProvider, DefaultCacheTTL) t.Run("minimum version check", func(t *testing.T) { err := Migrate(context.Background(), map[string]interface{}{ @@ -49,7 +52,7 @@ func TestMigrateSingleVersion(t *testing.T) { // Use the same datasource provider as the frontend test to ensure consistency dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - Initialize(dsProvider, leProvider) + Initialize(dsProvider, leProvider, DefaultCacheTTL) runSingleVersionMigrationTests(t, SINGLE_VERSION_OUTPUT_DIR) } @@ -218,7 +221,7 @@ func TestSchemaMigrationMetrics(t *testing.T) { // Initialize migration with test providers dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - Initialize(dsProvider, leProvider) + Initialize(dsProvider, leProvider, DefaultCacheTTL) // Create a test registry for metrics registry := prometheus.NewRegistry() @@ -304,7 +307,7 @@ func TestSchemaMigrationMetrics(t *testing.T) { func TestSchemaMigrationLogging(t *testing.T) { dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - Initialize(dsProvider, leProvider) + Initialize(dsProvider, leProvider, DefaultCacheTTL) tests := []struct { name string @@ -423,7 +426,7 @@ func TestMigrateDevDashboards(t *testing.T) { ResetForTesting() dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.DevDashboardConfig) leProvider := migrationtestutil.NewLibraryElementProvider() - Initialize(dsProvider, leProvider) + Initialize(dsProvider, leProvider, DefaultCacheTTL) runDevDashboardMigrationTests(t, schemaversion.LATEST_VERSION, DEV_DASHBOARDS_OUTPUT_DIR) } @@ -449,3 +452,232 @@ func runDevDashboardMigrationTests(t *testing.T, targetVersion int, outputDir st }) } } + +func TestMigrateWithCache(t *testing.T) { + // Reset the migration singleton before each test + ResetForTesting() + datasources := []schemaversion.DataSourceInfo{ + {UID: "ds-uid-1", Type: "prometheus", Name: "Prometheus", Default: true, APIVersion: "v1"}, + {UID: "ds-uid-2", Type: "loki", Name: "Loki", Default: false, APIVersion: "v1"}, + {UID: "ds-uid-3", Type: "prometheus", Name: "Prometheus 2", Default: false, APIVersion: "v1"}, + } + + // Create a dashboard at schema version 32 for V33 and V36 migration with datasource references + dashboard1 := map[string]interface{}{ + "schemaVersion": 32, + "title": "Test Dashboard 1", + "panels": []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "timeseries", + // String datasource that V33 will migrate to object reference + "datasource": "Prometheus", + "targets": []interface{}{ + map[string]interface{}{ + "refId": "A", + "datasource": "Loki", + }, + }, + }, + }, + } + + // Create a dashboard at schema version 35 for testing V36 migration with datasource references in annotations + dashboard2 := map[string]interface{}{ + "schemaVersion": 35, + "title": "Test Dashboard 2", + "annotations": map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "name": "Test Annotation", + "datasource": "Prometheus 2", // String reference that V36 should convert + "enable": true, + }, + }, + }, + } + + t.Run("with datasources", func(t *testing.T) { + ResetForTesting() + dsProvider := newCountingProvider(datasources) + leProvider := newCountingLibraryProvider(nil) + // Initialize the migration system with our counting providers + Initialize(dsProvider, leProvider, DefaultCacheTTL) + // Verify initial call count is zero + assert.Equal(t, dsProvider.GetCallCount(), int64(0)) + // Create a context with namespace (required for caching) + ctx := request.WithNamespace(context.Background(), "default") + + // First migration - should invoke the provider once to build the cache + dash1 := deepCopyDashboard(dashboard1) + err := Migrate(ctx, dash1, schemaversion.LATEST_VERSION) + require.NoError(t, err) + assert.Equal(t, int64(1), dsProvider.GetCallCount()) + + // Verify datasource conversion from string to object reference + panels := dash1["panels"].([]interface{}) + panel := panels[0].(map[string]interface{}) + panelDS, ok := panel["datasource"].(map[string]interface{}) + require.True(t, ok, "panel datasource should be converted to object") + assert.Equal(t, "ds-uid-1", panelDS["uid"]) + assert.Equal(t, "prometheus", panelDS["type"]) + + // Verify target datasource conversion + targets := panel["targets"].([]interface{}) + target := targets[0].(map[string]interface{}) + targetDS, ok := target["datasource"].(map[string]interface{}) + require.True(t, ok, "target datasource should be converted to object") + assert.Equal(t, "ds-uid-2", targetDS["uid"]) + assert.Equal(t, "loki", targetDS["type"]) + + // Migration with V35 dashboard - should use the cached index from first migration + dash2 := deepCopyDashboard(dashboard2) + err = Migrate(ctx, dash2, schemaversion.LATEST_VERSION) + require.NoError(t, err, "second migration should succeed") + assert.Equal(t, int64(1), dsProvider.GetCallCount()) + + // Verify the annotation datasource was converted to object reference + annotations := dash2["annotations"].(map[string]interface{}) + list := annotations["list"].([]interface{}) + var testAnnotation map[string]interface{} + for _, a := range list { + ann := a.(map[string]interface{}) + if ann["name"] == "Test Annotation" { + testAnnotation = ann + break + } + } + require.NotNil(t, testAnnotation, "Test Annotation should exist") + annotationDS, ok := testAnnotation["datasource"].(map[string]interface{}) + require.True(t, ok, "annotation datasource should be converted to object") + assert.Equal(t, "ds-uid-3", annotationDS["uid"]) + assert.Equal(t, "prometheus", annotationDS["type"]) + }) + + // tests that cache isolates data per namespace + t.Run("with multiple orgs", func(t *testing.T) { + // Reset the migration singleton + ResetForTesting() + dsProvider := newCountingProvider(datasources) + leProvider := newCountingLibraryProvider(nil) + Initialize(dsProvider, leProvider, DefaultCacheTTL) + // Create contexts for different orgs with proper namespace format (org-ID) + ctx1 := request.WithNamespace(context.Background(), "default") // org 1 + ctx2 := request.WithNamespace(context.Background(), "stacks-2") // stack 2 + + // Migrate for org 1 + err := Migrate(ctx1, deepCopyDashboard(dashboard1), schemaversion.LATEST_VERSION) + require.NoError(t, err) + callsAfterOrg1 := dsProvider.GetCallCount() + + // Migrate for org 2 - should build separate cache + err = Migrate(ctx2, deepCopyDashboard(dashboard2), schemaversion.LATEST_VERSION) + require.NoError(t, err) + callsAfterOrg2 := dsProvider.GetCallCount() + + assert.Greater(t, callsAfterOrg2, callsAfterOrg1, + "org 2 migration should have called provider (separate cache)") + + // Migrate again for org 1 - should use cache + err = Migrate(ctx1, deepCopyDashboard(dashboard1), schemaversion.LATEST_VERSION) + require.NoError(t, err) + callsAfterOrg1Again := dsProvider.GetCallCount() + assert.Equal(t, callsAfterOrg2, callsAfterOrg1Again, + "second org 1 migration should use cache") + + // Migrate again for org 2 - should use cache + err = Migrate(ctx2, deepCopyDashboard(dashboard1), schemaversion.LATEST_VERSION) + require.NoError(t, err) + callsAfterOrg2Again := dsProvider.GetCallCount() + assert.Equal(t, callsAfterOrg2, callsAfterOrg2Again, + "second org 2 migration should use cache") + }) +} + +// countingProvider wraps a datasource provider and counts calls to Index() +type countingProvider struct { + datasources []schemaversion.DataSourceInfo + callCount atomic.Int64 +} + +func newCountingProvider(datasources []schemaversion.DataSourceInfo) *countingProvider { + return &countingProvider{ + datasources: datasources, + } +} + +func (p *countingProvider) Index(_ context.Context) *schemaversion.DatasourceIndex { + p.callCount.Add(1) + return schemaversion.NewDatasourceIndex(p.datasources) +} + +func (p *countingProvider) GetCallCount() int64 { + return p.callCount.Load() +} + +// countingLibraryProvider wraps a library element provider and counts calls +type countingLibraryProvider struct { + elements []schemaversion.LibraryElementInfo + callCount atomic.Int64 +} + +func newCountingLibraryProvider(elements []schemaversion.LibraryElementInfo) *countingLibraryProvider { + return &countingLibraryProvider{ + elements: elements, + } +} + +func (p *countingLibraryProvider) GetLibraryElementInfo(_ context.Context) []schemaversion.LibraryElementInfo { + p.callCount.Add(1) + return p.elements +} + +func (p *countingLibraryProvider) GetCallCount() int64 { + return p.callCount.Load() +} + +// deepCopyDashboard creates a deep copy of a dashboard map +func deepCopyDashboard(dash map[string]interface{}) map[string]interface{} { + cpy := make(map[string]interface{}) + for k, v := range dash { + switch val := v.(type) { + case []interface{}: + cpy[k] = deepCopySlice(val) + case map[string]interface{}: + cpy[k] = deepCopyMapForCache(val) + default: + cpy[k] = v + } + } + return cpy +} + +func deepCopySlice(s []interface{}) []interface{} { + cpy := make([]interface{}, len(s)) + for i, v := range s { + switch val := v.(type) { + case []interface{}: + cpy[i] = deepCopySlice(val) + case map[string]interface{}: + cpy[i] = deepCopyMapForCache(val) + default: + cpy[i] = v + } + } + return cpy +} + +func deepCopyMapForCache(m map[string]interface{}) map[string]interface{} { + cpy := make(map[string]interface{}) + for k, v := range m { + switch val := v.(type) { + case []interface{}: + cpy[k] = deepCopySlice(val) + case map[string]interface{}: + cpy[k] = deepCopyMapForCache(val) + default: + cpy[k] = v + } + } + return cpy +} diff --git a/apps/dashboard/pkg/migration/schemaversion/cache.go b/apps/dashboard/pkg/migration/schemaversion/cache.go new file mode 100644 index 00000000000..f31eaa14e1a --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/cache.go @@ -0,0 +1,104 @@ +package schemaversion + +import ( + "context" + "sync" + "time" + + "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/hashicorp/golang-lru/v2/expirable" + k8srequest "k8s.io/apiserver/pkg/endpoints/request" + + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" +) + +const defaultCacheSize = 1000 + +// CacheProvider is a generic cache interface for schema version providers. +type CacheProvider[T any] interface { + // Get returns the cached value if it's still valid, otherwise calls fetch and caches the result. + Get(ctx context.Context) T +} + +// PreloadableCache is an interface for providers that support preloading the cache. +type PreloadableCache interface { + // Preload loads data into the cache for the given namespaces. + Preload(ctx context.Context, nsInfos []types.NamespaceInfo) +} + +// cachedProvider is a thread-safe TTL cache that wraps any fetch function. +type cachedProvider[T any] struct { + fetch func(context.Context) T + cache *expirable.LRU[string, T] // LRU cache: namespace to cache entry + inFlight sync.Map // map[string]*sync.Mutex - per-namespace fetch locks + logger log.Logger +} + +// newCachedProvider creates a new cachedProvider. +// The fetch function should be able to handle context with different namespaces. +// A non-positive size turns LRU mechanism off (cache of unlimited size). +// A non-positive cacheTTL disables TTL expiration. +func newCachedProvider[T any](fetch func(context.Context) T, size int, cacheTTL time.Duration, logger log.Logger) *cachedProvider[T] { + cacheProvider := &cachedProvider[T]{ + fetch: fetch, + logger: logger, + } + cacheProvider.cache = expirable.NewLRU(size, func(key string, value T) { + cacheProvider.inFlight.Delete(key) + }, cacheTTL) + return cacheProvider +} + +// Get returns the cached value if it's still valid, otherwise calls fetch and caches the result. +func (p *cachedProvider[T]) Get(ctx context.Context) T { + // Get namespace info from ctx + nsInfo, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + // No namespace, fall back to direct fetch call without caching + p.logger.Warn("Unable to get namespace info from context, skipping cache", "error", err) + return p.fetch(ctx) + } + + namespace := nsInfo.Value + // Fast path: check if cache is still valid + if entry, ok := p.cache.Get(namespace); ok { + return entry + } + + // Get or create a per-namespace lock for this fetch operation + // This ensures only one fetch happens per namespace at a time + lockInterface, _ := p.inFlight.LoadOrStore(namespace, &sync.Mutex{}) + nsMutex := lockInterface.(*sync.Mutex) + + // Lock this specific namespace - other namespaces can still proceed + nsMutex.Lock() + defer nsMutex.Unlock() + + // Double-check: another goroutine might have already fetched while we waited + if entry, ok := p.cache.Get(namespace); ok { + return entry + } + + // Fetch outside the main lock - only this namespace is blocked + p.logger.Debug("cache miss or expired, fetching new value", "namespace", namespace) + value := p.fetch(ctx) + + // Update the cache for this namespace + p.cache.Add(namespace, value) + + return value +} + +// Preload loads data into the cache for the given namespaces. +func (p *cachedProvider[T]) Preload(ctx context.Context, nsInfos []types.NamespaceInfo) { + // Build the cache using a context with the namespace + p.logger.Info("preloading cache", "nsInfos", len(nsInfos)) + startedAt := time.Now() + defer func() { + p.logger.Info("finished preloading cache", "nsInfos", len(nsInfos), "elapsed", time.Since(startedAt)) + }() + for _, nsInfo := range nsInfos { + p.cache.Add(nsInfo.Value, p.fetch(k8srequest.WithNamespace(ctx, nsInfo.Value))) + } +} diff --git a/apps/dashboard/pkg/migration/schemaversion/cache_test.go b/apps/dashboard/pkg/migration/schemaversion/cache_test.go new file mode 100644 index 00000000000..f044ab0f813 --- /dev/null +++ b/apps/dashboard/pkg/migration/schemaversion/cache_test.go @@ -0,0 +1,478 @@ +package schemaversion + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/endpoints/request" +) + +// testProvider tracks how many times get() is called +type testProvider struct { + testData any + callCount atomic.Int64 +} + +func newTestProvider(testData any) *testProvider { + return &testProvider{ + testData: testData, + } +} + +func (p *testProvider) get(_ context.Context) any { + p.callCount.Add(1) + return p.testData +} + +func (p *testProvider) getCallCount() int64 { + return p.callCount.Load() +} + +func TestCachedProvider_CacheHit(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + {UID: "ds2", Type: "loki", Name: "Loki"}, + } + + underlying := newTestProvider(datasources) + // Test newCachedProvider directly instead of the wrapper + cached := newCachedProvider(underlying.get, defaultCacheSize, time.Minute, log.New("test")) + + // Use "default" namespace (org 1) - this is the standard Grafana namespace format + ctx := request.WithNamespace(context.Background(), "default") + + // First call should hit the underlying provider + idx1 := cached.Get(ctx) + require.NotNil(t, idx1) + assert.Equal(t, int64(1), underlying.getCallCount(), "first call should invoke underlying provider") + + // Second call should use cache + idx2 := cached.Get(ctx) + require.NotNil(t, idx2) + assert.Equal(t, int64(1), underlying.getCallCount(), "second call should use cache, not invoke underlying provider") + + // Both should return the same data + assert.Equal(t, idx1, idx2) +} + +func TestCachedProvider_NamespaceIsolation(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + + underlying := newTestProvider(datasources) + cached := newCachedProvider(underlying.get, defaultCacheSize, time.Minute, log.New("test")) + + // Use "default" (org 1) and "org-2" (org 2) - standard Grafana namespace formats + ctx1 := request.WithNamespace(context.Background(), "default") + ctx2 := request.WithNamespace(context.Background(), "org-2") + + // First call for org 1 + idx1 := cached.Get(ctx1) + require.NotNil(t, idx1) + assert.Equal(t, int64(1), underlying.getCallCount(), "first org-1 call should invoke underlying provider") + + // Call for org 2 should also invoke underlying provider (different namespace) + idx2 := cached.Get(ctx2) + require.NotNil(t, idx2) + assert.Equal(t, int64(2), underlying.getCallCount(), "org-2 call should invoke underlying provider (separate cache)") + + // Second call for org 1 should use cache + idx3 := cached.Get(ctx1) + require.NotNil(t, idx3) + assert.Equal(t, int64(2), underlying.getCallCount(), "second org-1 call should use cache") + + // Second call for org 2 should use cache + idx4 := cached.Get(ctx2) + require.NotNil(t, idx4) + assert.Equal(t, int64(2), underlying.getCallCount(), "second org-2 call should use cache") +} + +func TestCachedProvider_NoNamespaceFallback(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + + underlying := newTestProvider(datasources) + cached := newCachedProvider(underlying.get, defaultCacheSize, time.Minute, log.New("test")) + + // Context without namespace - should fall back to direct provider call + ctx := context.Background() + + idx1 := cached.Get(ctx) + require.NotNil(t, idx1) + assert.Equal(t, int64(1), underlying.getCallCount()) + + // Second call without namespace should also invoke underlying (no caching for unknown namespace) + idx2 := cached.Get(ctx) + require.NotNil(t, idx2) + assert.Equal(t, int64(2), underlying.getCallCount(), "without namespace, each call should invoke underlying provider") +} + +func TestCachedProvider_ConcurrentAccess(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + + underlying := newTestProvider(datasources) + cached := newCachedProvider(underlying.get, defaultCacheSize, time.Minute, log.New("test")) + + // Use "default" namespace (org 1) + ctx := request.WithNamespace(context.Background(), "default") + + var wg sync.WaitGroup + numGoroutines := 100 + + // Launch many goroutines that all try to access the cache simultaneously + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + idx := cached.Get(ctx) + require.NotNil(t, idx) + }() + } + + wg.Wait() + + // Due to double-check locking, only 1 goroutine should have actually built the cache + // In practice, there might be a few more due to timing, but it should be much less than numGoroutines + callCount := underlying.getCallCount() + assert.LessOrEqual(t, callCount, int64(5), "with proper locking, very few goroutines should invoke underlying provider; got %d", callCount) +} + +func TestCachedProvider_ConcurrentNamespaces(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + + underlying := newTestProvider(datasources) + cached := newCachedProvider(underlying.get, defaultCacheSize, time.Minute, log.New("test")) + + var wg sync.WaitGroup + numOrgs := 10 + callsPerOrg := 20 + + // Launch goroutines for multiple namespaces + // Use valid namespace formats: "default" for org 1, "org-N" for N > 1 + namespaces := make([]string, numOrgs) + namespaces[0] = "default" + for i := 1; i < numOrgs; i++ { + namespaces[i] = fmt.Sprintf("org-%d", i+1) + } + + for _, ns := range namespaces { + ctx := request.WithNamespace(context.Background(), ns) + for i := 0; i < callsPerOrg; i++ { + wg.Add(1) + go func(ctx context.Context) { + defer wg.Done() + idx := cached.Get(ctx) + require.NotNil(t, idx) + }(ctx) + } + } + + wg.Wait() + + // Each org should have at most a few calls (ideally 1, but timing can cause a few more) + callCount := underlying.getCallCount() + // With 10 orgs, we expect around 10 calls (one per org) + assert.LessOrEqual(t, callCount, int64(numOrgs), "expected roughly one call per org, got %d calls for %d orgs", callCount, numOrgs) +} + +// Test that cache returns correct data for each namespace +func TestCachedProvider_CorrectDataPerNamespace(t *testing.T) { + // Provider that returns different data based on namespace + underlying := &namespaceAwareProvider{ + datasourcesByNamespace: map[string][]DataSourceInfo{ + "default": {{UID: "org1-ds", Type: "prometheus", Name: "Org1 DS", Default: true}}, + "org-2": {{UID: "org2-ds", Type: "loki", Name: "Org2 DS", Default: true}}, + }, + } + cached := newCachedProvider(underlying.Index, defaultCacheSize, time.Minute, log.New("test")) + + // Use valid namespace formats + ctx1 := request.WithNamespace(context.Background(), "default") + ctx2 := request.WithNamespace(context.Background(), "org-2") + + idx1 := cached.Get(ctx1) + idx2 := cached.Get(ctx2) + + assert.Equal(t, "org1-ds", idx1.GetDefault().UID, "org 1 should get org-1 datasources") + assert.Equal(t, "org2-ds", idx2.GetDefault().UID, "org 2 should get org-2 datasources") + + // Subsequent calls should still return correct data + idx1Again := cached.Get(ctx1) + idx2Again := cached.Get(ctx2) + + assert.Equal(t, "org1-ds", idx1Again.GetDefault().UID, "org 1 should still get org-1 datasources from cache") + assert.Equal(t, "org2-ds", idx2Again.GetDefault().UID, "org 2 should still get org-2 datasources from cache") +} + +// TestCachedProvider_PreloadMultipleNamespaces verifies preloading multiple namespaces +func TestCachedProvider_PreloadMultipleNamespaces(t *testing.T) { + // Provider that returns different data based on namespace + underlying := &namespaceAwareProvider{ + datasourcesByNamespace: map[string][]DataSourceInfo{ + "default": {{UID: "org1-ds", Type: "prometheus", Name: "Org1 DS", Default: true}}, + "org-2": {{UID: "org2-ds", Type: "loki", Name: "Org2 DS", Default: true}}, + "org-3": {{UID: "org3-ds", Type: "tempo", Name: "Org3 DS", Default: true}}, + }, + } + cached := newCachedProvider(underlying.Index, defaultCacheSize, time.Minute, log.New("test")) + + // Preload multiple namespaces + nsInfos := []authlib.NamespaceInfo{ + createNamespaceInfo(1, 0, "default"), + createNamespaceInfo(2, 0, "org-2"), + createNamespaceInfo(3, 0, "org-3"), + } + cached.Preload(context.Background(), nsInfos) + + // After preload, the underlying provider should have been called once per namespace + assert.Equal(t, 3, underlying.callCount, "preload should call underlying provider once per namespace") + + // Access all namespaces - should use preloaded data and get correct data per namespace + expectedUIDs := map[string]string{ + "default": "org1-ds", + "org-2": "org2-ds", + "org-3": "org3-ds", + } + + for _, ns := range []string{"default", "org-2", "org-3"} { + ctx := request.WithNamespace(context.Background(), ns) + idx := cached.Get(ctx) + require.NotNil(t, idx, "index for namespace %s should not be nil", ns) + assert.Equal(t, expectedUIDs[ns], idx.GetDefault().UID, "namespace %s should get correct datasource", ns) + } + + // The underlying provider should still have been called only 3 times (from preload) + assert.Equal(t, 3, underlying.callCount, + "access after preload should use cached data for all namespaces") +} + +// namespaceAwareProvider returns different datasources based on namespace +type namespaceAwareProvider struct { + datasourcesByNamespace map[string][]DataSourceInfo + callCount int +} + +func (p *namespaceAwareProvider) Index(ctx context.Context) *DatasourceIndex { + p.callCount++ + ns := request.NamespaceValue(ctx) + if ds, ok := p.datasourcesByNamespace[ns]; ok { + return NewDatasourceIndex(ds) + } + return NewDatasourceIndex(nil) +} + +// createNamespaceInfo creates a NamespaceInfo for testing +func createNamespaceInfo(orgID, stackID int64, value string) authlib.NamespaceInfo { + return authlib.NamespaceInfo{ + OrgID: orgID, + StackID: stackID, + Value: value, + } +} + +// Test DatasourceIndex functionality +func TestDatasourceIndex_Lookup(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds-uid-1", Type: "prometheus", Name: "Prometheus DS", Default: true, APIVersion: "v1"}, + {UID: "ds-uid-2", Type: "loki", Name: "Loki DS", Default: false, APIVersion: "v1"}, + } + idx := NewDatasourceIndex(datasources) + + t.Run("lookup by name", func(t *testing.T) { + ds := idx.Lookup("Prometheus DS") + require.NotNil(t, ds) + assert.Equal(t, "ds-uid-1", ds.UID) + }) + + t.Run("lookup by UID", func(t *testing.T) { + ds := idx.Lookup("ds-uid-2") + require.NotNil(t, ds) + assert.Equal(t, "Loki DS", ds.Name) + }) + + t.Run("lookup unknown returns nil", func(t *testing.T) { + ds := idx.Lookup("unknown") + assert.Nil(t, ds) + }) + + t.Run("get default", func(t *testing.T) { + ds := idx.GetDefault() + require.NotNil(t, ds) + assert.Equal(t, "ds-uid-1", ds.UID) + }) + + t.Run("lookup by UID directly", func(t *testing.T) { + ds := idx.LookupByUID("ds-uid-1") + require.NotNil(t, ds) + assert.Equal(t, "Prometheus DS", ds.Name) + }) + + t.Run("lookup by name directly", func(t *testing.T) { + ds := idx.LookupByName("Loki DS") + require.NotNil(t, ds) + assert.Equal(t, "ds-uid-2", ds.UID) + }) +} + +func TestDatasourceIndex_EmptyIndex(t *testing.T) { + idx := NewDatasourceIndex(nil) + + assert.Nil(t, idx.GetDefault()) + assert.Nil(t, idx.Lookup("anything")) + assert.Nil(t, idx.LookupByUID("anything")) + assert.Nil(t, idx.LookupByName("anything")) +} + +// TestCachedProvider_TTLExpiration verifies that cache expires after TTL +func TestCachedProvider_TTLExpiration(t *testing.T) { + datasources := []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + } + + underlying := newTestProvider(datasources) + // Use a very short TTL for testing + shortTTL := 50 * time.Millisecond + cached := newCachedProvider(underlying.get, defaultCacheSize, shortTTL, log.New("test")) + + ctx := request.WithNamespace(context.Background(), "default") + + // First call - should call underlying provider + idx1 := cached.Get(ctx) + require.NotNil(t, idx1) + assert.Equal(t, int64(1), underlying.getCallCount(), "first call should invoke underlying provider") + + // Second call immediately - should use cache + idx2 := cached.Get(ctx) + require.NotNil(t, idx2) + assert.Equal(t, int64(1), underlying.getCallCount(), "second call should use cache") + + // Wait for TTL to expire + time.Sleep(shortTTL + 20*time.Millisecond) + + // Third call after TTL - should call underlying provider again + idx3 := cached.Get(ctx) + require.NotNil(t, idx3) + assert.Equal(t, int64(2), underlying.getCallCount(), + "after TTL expiration, underlying provider should be called again") +} + +// TestCachedProvider_ParallelNamespacesFetch verifies that different namespaces can fetch in parallel +func TestCachedProvider_ParallelNamespacesFetch(t *testing.T) { + // Create a blocking provider that tracks concurrent executions + provider := &blockingProvider{ + blockDuration: 100 * time.Millisecond, + datasources: []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + }, + } + cached := newCachedProvider(provider.get, defaultCacheSize, time.Minute, log.New("test")) + + numNamespaces := 5 + var wg sync.WaitGroup + + // Launch fetches for different namespaces simultaneously + startTime := time.Now() + for i := 0; i < numNamespaces; i++ { + wg.Add(1) + namespace := fmt.Sprintf("org-%d", i+1) + go func(ns string) { + defer wg.Done() + ctx := request.WithNamespace(context.Background(), ns) + idx := cached.Get(ctx) + require.NotNil(t, idx) + }(namespace) + } + wg.Wait() + elapsed := time.Since(startTime) + + // Verify that all namespaces were called + assert.Equal(t, int64(numNamespaces), provider.callCount.Load()) + + // Verify max concurrent executions shows parallelism + maxConcurrent := provider.maxConcurrent.Load() + assert.Equal(t, int64(numNamespaces), maxConcurrent) + + // If all namespaces had to wait sequentially, it would take numNamespaces * blockDuration + // With parallelism, it should be much faster (close to just blockDuration) + sequentialTime := time.Duration(numNamespaces) * provider.blockDuration + assert.Less(t, elapsed, sequentialTime) +} + +// TestCachedProvider_SameNamespaceSerialFetch verifies that the same namespace doesn't fetch concurrently +func TestCachedProvider_SameNamespaceSerialFetch(t *testing.T) { + // Create a blocking provider that tracks concurrent executions + provider := &blockingProvider{ + blockDuration: 100 * time.Millisecond, + datasources: []DataSourceInfo{ + {UID: "ds1", Type: "prometheus", Name: "Prometheus", Default: true}, + }, + } + cached := newCachedProvider(provider.get, defaultCacheSize, time.Minute, log.New("test")) + + numGoroutines := 10 + var wg sync.WaitGroup + + // Launch multiple fetches for the SAME namespace simultaneously + ctx := request.WithNamespace(context.Background(), "default") + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + idx := cached.Get(ctx) + require.NotNil(t, idx) + }() + } + wg.Wait() + + // Max concurrent should be 1 since all goroutines are for the same namespace + maxConcurrent := provider.maxConcurrent.Load() + assert.Equal(t, int64(1), maxConcurrent) +} + +// blockingProvider is a test provider that simulates slow fetch operations +// and tracks concurrent executions +type blockingProvider struct { + blockDuration time.Duration + datasources []DataSourceInfo + callCount atomic.Int64 + currentActive atomic.Int64 + maxConcurrent atomic.Int64 +} + +func (p *blockingProvider) get(_ context.Context) any { + p.callCount.Add(1) + + // Track concurrent executions + current := p.currentActive.Add(1) + + // Update max concurrent if this is a new peak + for { + maxVal := p.maxConcurrent.Load() + if current <= maxVal { + break + } + if p.maxConcurrent.CompareAndSwap(maxVal, current) { + break + } + } + + // Simulate slow operation + time.Sleep(p.blockDuration) + + p.currentActive.Add(-1) + return p.datasources +} diff --git a/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go b/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go index 18bc2fc17c4..9deee29b414 100644 --- a/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go +++ b/apps/dashboard/pkg/migration/schemaversion/datasource_utils.go @@ -2,8 +2,9 @@ package schemaversion import ( "context" - "sync" "time" + + "github.com/grafana/grafana/pkg/infra/log" ) // Shared utility functions for datasource migrations across different schema versions. @@ -11,65 +12,41 @@ import ( // string names/UIDs to structured reference objects with uid, type, and apiVersion. // cachedIndexProvider wraps a DataSourceIndexProvider with time-based caching. -// This prevents multiple DB queries and index builds during operations that may call -// provider.Index() multiple times (e.g., dashboard conversions with many datasource lookups). -// The cache expires after 10 seconds, allowing it to be used as a long-lived singleton -// while still refreshing periodically. -// -// Thread-safe: Uses sync.RWMutex to guarantee safe concurrent access. type cachedIndexProvider struct { - provider DataSourceIndexProvider - mu sync.RWMutex - index *DatasourceIndex - cachedAt time.Time - cacheTTL time.Duration + *cachedProvider[*DatasourceIndex] } -// Index returns the cached index if it's still valid (< 10s old), otherwise rebuilds it. -// Uses RWMutex for efficient concurrent reads when cache is valid. +// Index returns the cached index if it's still valid (< TTL old), otherwise rebuilds it. func (p *cachedIndexProvider) Index(ctx context.Context) *DatasourceIndex { - // Fast path: check if cache is still valid using read lock - p.mu.RLock() - if p.index != nil && time.Since(p.cachedAt) < p.cacheTTL { - idx := p.index - p.mu.RUnlock() - return idx - } - p.mu.RUnlock() - - // Slow path: cache expired or not yet built, acquire write lock - p.mu.Lock() - defer p.mu.Unlock() - - // Double-check: another goroutine might have refreshed the cache - // while we were waiting for the write lock - if p.index != nil && time.Since(p.cachedAt) < p.cacheTTL { - return p.index - } - - // Rebuild the cache - p.index = p.provider.Index(ctx) - p.cachedAt = time.Now() - return p.index + return p.Get(ctx) } -// WrapIndexProviderWithCache wraps a provider to cache the index with a 10-second TTL. -// Useful for conversions or migrations that may call provider.Index() multiple times. -// The cache expires after 10 seconds, making it suitable for use as a long-lived singleton -// at the top level of dependency injection while still refreshing periodically. -// -// Example usage in dashboard conversion: -// -// cachedDsIndexProvider := schemaversion.WrapIndexProviderWithCache(dsIndexProvider) -// // Now all calls to cachedDsIndexProvider.Index(ctx) return the same cached index -// // for up to 10 seconds before refreshing -func WrapIndexProviderWithCache(provider DataSourceIndexProvider) DataSourceIndexProvider { - if provider == nil { - return nil +// cachedLibraryElementProvider wraps a LibraryElementIndexProvider with time-based caching. +type cachedLibraryElementProvider struct { + *cachedProvider[[]LibraryElementInfo] +} + +func (p *cachedLibraryElementProvider) GetLibraryElementInfo(ctx context.Context) []LibraryElementInfo { + return p.Get(ctx) +} + +// WrapIndexProviderWithCache wraps a DataSourceIndexProvider to cache indexes with a configurable TTL. +func WrapIndexProviderWithCache(provider DataSourceIndexProvider, cacheTTL time.Duration) DataSourceIndexProvider { + if provider == nil || cacheTTL <= 0 { + return provider } return &cachedIndexProvider{ - provider: provider, - cacheTTL: 10 * time.Second, + newCachedProvider[*DatasourceIndex](provider.Index, defaultCacheSize, cacheTTL, log.New("schemaversion.dsindexprovider")), + } +} + +// WrapLibraryElementProviderWithCache wraps a LibraryElementIndexProvider to cache library elements with a configurable TTL. +func WrapLibraryElementProviderWithCache(provider LibraryElementIndexProvider, cacheTTL time.Duration) LibraryElementIndexProvider { + if provider == nil || cacheTTL <= 0 { + return provider + } + return &cachedLibraryElementProvider{ + newCachedProvider[[]LibraryElementInfo](provider.GetLibraryElementInfo, defaultCacheSize, cacheTTL, log.New("schemaversion.leindexprovider")), } } @@ -216,60 +193,3 @@ func MigrateDatasourceNameToRef(nameOrRef interface{}, options map[string]bool, return nil } - -// cachedLibraryElementProvider wraps a LibraryElementIndexProvider with time-based caching. -// This prevents multiple DB queries during operations that may call GetLibraryElementInfo() -// multiple times (e.g., dashboard conversions with many library panel lookups). -// The cache expires after 10 seconds, allowing it to be used as a long-lived singleton -// while still refreshing periodically. -// -// Thread-safe: Uses sync.RWMutex to guarantee safe concurrent access. -type cachedLibraryElementProvider struct { - provider LibraryElementIndexProvider - mu sync.RWMutex - elements []LibraryElementInfo - cachedAt time.Time - cacheTTL time.Duration -} - -// GetLibraryElementInfo returns the cached library elements if they're still valid (< 10s old), otherwise rebuilds the cache. -// Uses RWMutex for efficient concurrent reads when cache is valid. -func (p *cachedLibraryElementProvider) GetLibraryElementInfo(ctx context.Context) []LibraryElementInfo { - // Fast path: check if cache is still valid using read lock - p.mu.RLock() - if p.elements != nil && time.Since(p.cachedAt) < p.cacheTTL { - elements := p.elements - p.mu.RUnlock() - return elements - } - p.mu.RUnlock() - - // Slow path: cache expired or not yet built, acquire write lock - p.mu.Lock() - defer p.mu.Unlock() - - // Double-check: another goroutine might have refreshed the cache - // while we were waiting for the write lock - if p.elements != nil && time.Since(p.cachedAt) < p.cacheTTL { - return p.elements - } - - // Rebuild the cache - p.elements = p.provider.GetLibraryElementInfo(ctx) - p.cachedAt = time.Now() - return p.elements -} - -// WrapLibraryElementProviderWithCache wraps a provider to cache library elements with a 10-second TTL. -// Useful for conversions or migrations that may call GetLibraryElementInfo() multiple times. -// The cache expires after 10 seconds, making it suitable for use as a long-lived singleton -// at the top level of dependency injection while still refreshing periodically. -func WrapLibraryElementProviderWithCache(provider LibraryElementIndexProvider) LibraryElementIndexProvider { - if provider == nil { - return nil - } - return &cachedLibraryElementProvider{ - provider: provider, - cacheTTL: 10 * time.Second, - } -} diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json index 863d7b2a102..c589f8b7400 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_new.v42.json @@ -76,9 +76,9 @@ "barGlow": false, "centerGlow": false, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -155,9 +155,9 @@ "barGlow": false, "centerGlow": true, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -234,9 +234,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -313,9 +313,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -392,9 +392,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -471,9 +471,9 @@ "barGlow": true, "centerGlow": true, "rounded": false, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -550,9 +550,9 @@ "barGlow": true, "centerGlow": true, "rounded": false, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -642,9 +642,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -721,9 +721,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -800,9 +800,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -879,9 +879,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -975,9 +975,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1054,9 +1054,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1133,9 +1133,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1212,9 +1212,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1291,9 +1291,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1387,9 +1387,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1470,9 +1470,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1553,9 +1553,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1645,10 +1645,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1731,10 +1731,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1831,10 +1831,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1918,11 +1918,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "sparkline": false, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2005,10 +2004,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2091,10 +2090,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -2147,4 +2146,4 @@ "title": "Panel tests - Gauge (new)", "uid": "panel-tests-gauge-new", "weekStart": "" -} \ No newline at end of file +} diff --git a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json index d4dd7980100..a3de6df336a 100644 --- a/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json +++ b/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-gauge/gauge_tests_old_to_new.v42.json @@ -956,9 +956,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -1162,4 +1162,4 @@ "title": "Panel tests - Old gauge to new", "uid": "panel-tests-old-gauge-to-new", "weekStart": "" -} \ No newline at end of file +} diff --git a/apps/example/kinds/manifest.cue b/apps/example/kinds/manifest.cue index 934d419623d..65947eaa151 100644 --- a/apps/example/kinds/manifest.cue +++ b/apps/example/kinds/manifest.cue @@ -34,7 +34,7 @@ manifest: { v0alpha1: { kinds: [examplev0alpha1] - // This is explicitly set to false to keep the example app disabled by default. + // This is explicitly set to false to keep the example app disabled by default. // It can be enabled via conf overrides, or by setting this value to true and regenerating. served: false } @@ -48,14 +48,14 @@ v1alpha1: { // served indicates whether this particular version is served by the API server. // served should be set to false before a version is removed from the manifest entirely. // served defaults to true if not present. - // This is explicitly set to false to keep the example app disabled by default. + // This is explicitly set to false to keep the example app disabled by default. // It can be enabled via conf overrides, or by setting this value to true and regenerating. served: false // routes contains resource routes for the version, which are split into 'namespaced' and 'cluster' scoped routes. // This allows you to add additional non-storage- and non-kind- based handlers for your app. // These should only be used if the behavior cannot be accomplished by reconciliation on storage events or subresource routes on a kind. routes: { - // namespaced contains namespace-scoped resource routes for the version, + // namespaced contains namespace-scoped resource routes for the version, // which are exposed as HTTP handlers on '/namespaces//'. namespaced: { "/something": { @@ -72,7 +72,7 @@ v1alpha1: { } } } - // cluster contains cluster-scoped resource routes for the version, + // cluster contains cluster-scoped resource routes for the version, // which are exposed as HTTP handlers on '/'. cluster: { "/other": { @@ -113,4 +113,4 @@ v1alpha1: { enabled: true } } -} \ No newline at end of file +} diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 7f2fd8f462f..0b22bce3774 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -499,8 +499,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= @@ -853,6 +853,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k= +github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDPMdshj3QMvpXn+wK4T8awF9n2sd8i4YRiGqX2xTvg= +github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s= @@ -1416,8 +1418,8 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3 h1:/4/IJi5iyTdh6mqOUaASW148HQpujYiHl0Wl78dSOSc= github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3/go.mod h1:aJIMhRsunltJR926EB2MUg8qHemFQDreSB33pyto2Ps= -github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= -github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE= +github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= diff --git a/apps/iam/pkg/apis/iam/v0alpha1/corerole_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/corerole_object_gen.go index 625cced10b9..9a99aadddee 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/corerole_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/corerole_object_gen.go @@ -23,6 +23,12 @@ type CoreRole struct { Spec CoreRoleSpec `json:"spec" yaml:"spec"` } +func NewCoreRole() *CoreRole { + return &CoreRole{ + Spec: *NewCoreRoleSpec(), + } +} + func (o *CoreRole) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/corerole_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/corerole_schema_gen.go index 637f78355f8..82c0a8102e0 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/corerole_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/corerole_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaCoreRole = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &CoreRole{}, &CoreRoleList{}, resource.WithKind("CoreRole"), + schemaCoreRole = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewCoreRole(), &CoreRoleList{}, resource.WithKind("CoreRole"), resource.WithPlural("coreroles"), resource.WithScope(resource.NamespacedScope)) kindCoreRole = resource.Kind{ Schema: schemaCoreRole, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_object_gen.go index db20616c355..bbc7c2f65c4 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_object_gen.go @@ -23,6 +23,12 @@ type ExternalGroupMapping struct { Spec ExternalGroupMappingSpec `json:"spec" yaml:"spec"` } +func NewExternalGroupMapping() *ExternalGroupMapping { + return &ExternalGroupMapping{ + Spec: *NewExternalGroupMappingSpec(), + } +} + func (o *ExternalGroupMapping) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_schema_gen.go index 91090a9b460..943c41d8e3e 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/externalgroupmapping_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaExternalGroupMapping = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &ExternalGroupMapping{}, &ExternalGroupMappingList{}, resource.WithKind("ExternalGroupMapping"), + schemaExternalGroupMapping = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewExternalGroupMapping(), &ExternalGroupMappingList{}, resource.WithKind("ExternalGroupMapping"), resource.WithPlural("externalgroupmappings"), resource.WithScope(resource.NamespacedScope)) kindExternalGroupMapping = resource.Kind{ Schema: schemaExternalGroupMapping, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_object_gen.go index 27165fe70bd..66ad21c83cf 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_object_gen.go @@ -23,6 +23,12 @@ type GlobalRole struct { Spec GlobalRoleSpec `json:"spec" yaml:"spec"` } +func NewGlobalRole() *GlobalRole { + return &GlobalRole{ + Spec: *NewGlobalRoleSpec(), + } +} + func (o *GlobalRole) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_schema_gen.go index ce3322f97c0..be220cb0e95 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaGlobalRole = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &GlobalRole{}, &GlobalRoleList{}, resource.WithKind("GlobalRole"), + schemaGlobalRole = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewGlobalRole(), &GlobalRoleList{}, resource.WithKind("GlobalRole"), resource.WithPlural("globalroles"), resource.WithScope(resource.NamespacedScope)) kindGlobalRole = resource.Kind{ Schema: schemaGlobalRole, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_object_gen.go index 3bd4609d25d..bb8b0644f62 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_object_gen.go @@ -23,6 +23,12 @@ type GlobalRoleBinding struct { Spec GlobalRoleBindingSpec `json:"spec" yaml:"spec"` } +func NewGlobalRoleBinding() *GlobalRoleBinding { + return &GlobalRoleBinding{ + Spec: *NewGlobalRoleBindingSpec(), + } +} + func (o *GlobalRoleBinding) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_schema_gen.go index 9b4e65aa5e6..2e39da946f9 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrolebinding_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaGlobalRoleBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &GlobalRoleBinding{}, &GlobalRoleBindingList{}, resource.WithKind("GlobalRoleBinding"), + schemaGlobalRoleBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewGlobalRoleBinding(), &GlobalRoleBindingList{}, resource.WithKind("GlobalRoleBinding"), resource.WithPlural("globalrolebindings"), resource.WithScope(resource.NamespacedScope)) kindGlobalRoleBinding = resource.Kind{ Schema: schemaGlobalRoleBinding, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_object_gen.go index 996beb7e002..8cc4c28c209 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_object_gen.go @@ -23,6 +23,12 @@ type ResourcePermission struct { Spec ResourcePermissionSpec `json:"spec" yaml:"spec"` } +func NewResourcePermission() *ResourcePermission { + return &ResourcePermission{ + Spec: *NewResourcePermissionSpec(), + } +} + func (o *ResourcePermission) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_schema_gen.go index 257255a58fe..aa709f0d42a 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaResourcePermission = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &ResourcePermission{}, &ResourcePermissionList{}, resource.WithKind("ResourcePermission"), + schemaResourcePermission = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewResourcePermission(), &ResourcePermissionList{}, resource.WithKind("ResourcePermission"), resource.WithPlural("resourcepermissions"), resource.WithScope(resource.NamespacedScope)) kindResourcePermission = resource.Kind{ Schema: schemaResourcePermission, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/role_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/role_object_gen.go index 20bb587157e..0673c1a17c5 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/role_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/role_object_gen.go @@ -23,6 +23,12 @@ type Role struct { Spec RoleSpec `json:"spec" yaml:"spec"` } +func NewRole() *Role { + return &Role{ + Spec: *NewRoleSpec(), + } +} + func (o *Role) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/role_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/role_schema_gen.go index 74cc8730026..3aacfa2e060 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/role_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/role_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaRole = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &Role{}, &RoleList{}, resource.WithKind("Role"), + schemaRole = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewRole(), &RoleList{}, resource.WithKind("Role"), resource.WithPlural("roles"), resource.WithScope(resource.NamespacedScope)) kindRole = resource.Kind{ Schema: schemaRole, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_object_gen.go index dfd7741e05a..996fdc3df65 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_object_gen.go @@ -23,6 +23,12 @@ type RoleBinding struct { Spec RoleBindingSpec `json:"spec" yaml:"spec"` } +func NewRoleBinding() *RoleBinding { + return &RoleBinding{ + Spec: *NewRoleBindingSpec(), + } +} + func (o *RoleBinding) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_schema_gen.go index abb2b3eddf9..0cf1d4dee8b 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/rolebinding_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaRoleBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &RoleBinding{}, &RoleBindingList{}, resource.WithKind("RoleBinding"), + schemaRoleBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewRoleBinding(), &RoleBindingList{}, resource.WithKind("RoleBinding"), resource.WithPlural("rolebindings"), resource.WithScope(resource.NamespacedScope)) kindRoleBinding = resource.Kind{ Schema: schemaRoleBinding, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go index fe3cd7f3609..e2cfc9f33a7 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_object_gen.go @@ -23,6 +23,12 @@ type ServiceAccount struct { Spec ServiceAccountSpec `json:"spec" yaml:"spec"` } +func NewServiceAccount() *ServiceAccount { + return &ServiceAccount{ + Spec: *NewServiceAccountSpec(), + } +} + func (o *ServiceAccount) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_schema_gen.go index ea48c864739..71b171e0756 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/serviceaccount_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaServiceAccount = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &ServiceAccount{}, &ServiceAccountList{}, resource.WithKind("ServiceAccount"), + schemaServiceAccount = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewServiceAccount(), &ServiceAccountList{}, resource.WithKind("ServiceAccount"), resource.WithPlural("serviceaccounts"), resource.WithScope(resource.NamespacedScope)) kindServiceAccount = resource.Kind{ Schema: schemaServiceAccount, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go index 4030bebb9d1..85b57abf9d5 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_object_gen.go @@ -23,6 +23,12 @@ type Team struct { Spec TeamSpec `json:"spec" yaml:"spec"` } +func NewTeam() *Team { + return &Team{ + Spec: *NewTeamSpec(), + } +} + func (o *Team) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/team_schema_gen.go index 7a4875cf2d5..299d846d10e 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/team_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaTeam = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &Team{}, &TeamList{}, resource.WithKind("Team"), + schemaTeam = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewTeam(), &TeamList{}, resource.WithKind("Team"), resource.WithPlural("teams"), resource.WithScope(resource.NamespacedScope)) kindTeam = resource.Kind{ Schema: schemaTeam, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go index a958c55f5e7..0d388192448 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_object_gen.go @@ -23,6 +23,12 @@ type TeamBinding struct { Spec TeamBindingSpec `json:"spec" yaml:"spec"` } +func NewTeamBinding() *TeamBinding { + return &TeamBinding{ + Spec: *NewTeamBindingSpec(), + } +} + func (o *TeamBinding) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_schema_gen.go index e3c2c11a8b5..d089b966e9b 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/teambinding_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/teambinding_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaTeamBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &TeamBinding{}, &TeamBindingList{}, resource.WithKind("TeamBinding"), + schemaTeamBinding = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewTeamBinding(), &TeamBindingList{}, resource.WithKind("TeamBinding"), resource.WithPlural("teambindings"), resource.WithScope(resource.NamespacedScope)) kindTeamBinding = resource.Kind{ Schema: schemaTeamBinding, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go index 373112a1d87..bd3bf8fae0e 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_object_gen.go @@ -23,6 +23,12 @@ type User struct { Spec UserSpec `json:"spec" yaml:"spec"` } +func NewUser() *User { + return &User{ + Spec: *NewUserSpec(), + } +} + func (o *User) GetSpec() any { return o.Spec } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/user_schema_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/user_schema_gen.go index a44d9dfcf2d..ba48b19c015 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/user_schema_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/user_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaUser = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", &User{}, &UserList{}, resource.WithKind("User"), + schemaUser = resource.NewSimpleSchema("iam.grafana.app", "v0alpha1", NewUser(), &UserList{}, resource.WithKind("User"), resource.WithPlural("users"), resource.WithScope(resource.NamespacedScope)) kindUser = resource.Kind{ Schema: schemaUser, diff --git a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go index a41d51e8d70..ece208e8d68 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go @@ -1,8 +1,3 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// Code generated by grafana-app-sdk. DO NOT EDIT. - package v0alpha1 import ( diff --git a/apps/iam/pkg/apis/iam_manifest.go b/apps/iam/pkg/apis/iam_manifest.go index 5c27c228ed2..0106984f7f2 100644 --- a/apps/iam/pkg/apis/iam_manifest.go +++ b/apps/iam/pkg/apis/iam_manifest.go @@ -109,6 +109,13 @@ var appManifestData = app.ManifestData{ "items": { SchemaProps: spec.SchemaProps{ Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + + Ref: spec.MustCreateRef("#/components/schemas/getGroupsExternalGroupMapping"), + }}, + }, }, }, "kind": { @@ -200,6 +207,13 @@ var appManifestData = app.ManifestData{ "hits": { SchemaProps: spec.SchemaProps{ Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + + Ref: spec.MustCreateRef("#/components/schemas/getSearchTeamsTeamHit"), + }}, + }, }, }, "kind": { diff --git a/apps/logsdrilldown/definitions/logsdrilldown-manifest.json b/apps/logsdrilldown/definitions/logsdrilldown-manifest.json new file mode 100644 index 00000000000..da34c0c70f2 --- /dev/null +++ b/apps/logsdrilldown/definitions/logsdrilldown-manifest.json @@ -0,0 +1,319 @@ +{ + "apiVersion": "apps.grafana.com/v1alpha2", + "kind": "AppManifest", + "metadata": { + "name": "logsdrilldown" + }, + "spec": { + "appName": "logsdrilldown", + "group": "logsdrilldown.grafana.app", + "versions": [ + { + "name": "v1alpha1", + "served": true, + "kinds": [ + { + "kind": "LogsDrilldown", + "plural": "LogsDrilldowns", + "scope": "Namespaced", + "schemas": { + "LogsDrilldown": { + "properties": { + "spec": { + "$ref": "#/components/schemas/spec" + }, + "status": { + "$ref": "#/components/schemas/status" + } + }, + "required": ["spec"] + }, + "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" + }, + "spec": { + "additionalProperties": false, + "properties": { + "defaultFields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "interceptDismissed": { + "type": "boolean" + }, + "prettifyJSON": { + "type": "boolean" + }, + "wrapLogMessage": { + "type": "boolean" + } + }, + "required": ["defaultFields", "prettifyJSON", "wrapLogMessage", "interceptDismissed"], + "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" + } + }, + "conversion": false + }, + { + "kind": "LogsDrilldownDefaults", + "plural": "LogsDrilldownDefaults", + "scope": "Namespaced", + "schemas": { + "LogsDrilldownDefaults": { + "properties": { + "spec": { + "$ref": "#/components/schemas/spec" + }, + "status": { + "$ref": "#/components/schemas/status" + } + }, + "required": ["spec"] + }, + "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" + }, + "spec": { + "additionalProperties": false, + "properties": { + "defaultFields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "interceptDismissed": { + "type": "boolean" + }, + "prettifyJSON": { + "type": "boolean" + }, + "wrapLogMessage": { + "type": "boolean" + } + }, + "required": ["defaultFields", "prettifyJSON", "wrapLogMessage", "interceptDismissed"], + "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" + } + }, + "conversion": false + }, + { + "kind": "LogsDrilldownDefaultColumns", + "plural": "LogsDrilldownDefaultColumns", + "scope": "Namespaced", + "schemas": { + "LogsDefaultColumnsLabel": { + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": ["key", "value"], + "type": "object" + }, + "LogsDefaultColumnsLabels": { + "items": { + "$ref": "#/components/schemas/LogsDefaultColumnsLabel" + }, + "type": "array" + }, + "LogsDefaultColumnsRecord": { + "additionalProperties": false, + "properties": { + "columns": { + "items": { + "type": "string" + }, + "type": "array" + }, + "labels": { + "$ref": "#/components/schemas/LogsDefaultColumnsLabels" + } + }, + "required": ["columns", "labels"], + "type": "object" + }, + "LogsDefaultColumnsRecords": { + "items": { + "$ref": "#/components/schemas/LogsDefaultColumnsRecord" + }, + "type": "array" + }, + "LogsDrilldownDefaultColumns": { + "properties": { + "spec": { + "$ref": "#/components/schemas/spec" + }, + "status": { + "$ref": "#/components/schemas/status" + } + }, + "required": ["spec"] + }, + "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" + }, + "spec": { + "additionalProperties": false, + "properties": { + "records": { + "$ref": "#/components/schemas/LogsDefaultColumnsRecords" + } + }, + "required": ["records"], + "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" + } + }, + "conversion": false + } + ] + } + ], + "preferredVersion": "v1alpha1" + } +} diff --git a/apps/logsdrilldown/definitions/logsdrilldown.logsdrilldown.grafana.app.json b/apps/logsdrilldown/definitions/logsdrilldown.logsdrilldown.grafana.app.json new file mode 100644 index 00000000000..e0259f421ad --- /dev/null +++ b/apps/logsdrilldown/definitions/logsdrilldown.logsdrilldown.grafana.app.json @@ -0,0 +1,92 @@ +{ + "kind": "CustomResourceDefinition", + "apiVersion": "apiextensions.k8s.io/v1", + "metadata": { + "name": "logsdrilldowns.logsdrilldown.grafana.app" + }, + "spec": { + "group": "logsdrilldown.grafana.app", + "versions": [ + { + "name": "v1alpha1", + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": { + "properties": { + "spec": { + "properties": { + "defaultFields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "interceptDismissed": { + "type": "boolean" + }, + "prettifyJSON": { + "type": "boolean" + }, + "wrapLogMessage": { + "type": "boolean" + } + }, + "required": ["defaultFields", "prettifyJSON", "wrapLogMessage", "interceptDismissed"], + "type": "object" + }, + "status": { + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "x-kubernetes-preserve-unknown-fields": true + }, + "operatorStates": { + "additionalProperties": { + "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", + "x-kubernetes-preserve-unknown-fields": true + }, + "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" + }, + "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" + } + }, + "required": ["spec"], + "type": "object" + } + }, + "subresources": { + "status": {} + } + } + ], + "names": { + "kind": "LogsDrilldown", + "plural": "logsdrilldowns" + }, + "scope": "Namespaced" + } +} diff --git a/apps/logsdrilldown/definitions/logsdrilldowndefaultcolumns.logsdrilldown.grafana.app.json b/apps/logsdrilldown/definitions/logsdrilldowndefaultcolumns.logsdrilldown.grafana.app.json new file mode 100644 index 00000000000..28aa314311d --- /dev/null +++ b/apps/logsdrilldown/definitions/logsdrilldowndefaultcolumns.logsdrilldown.grafana.app.json @@ -0,0 +1,107 @@ +{ + "kind": "CustomResourceDefinition", + "apiVersion": "apiextensions.k8s.io/v1", + "metadata": { + "name": "logsdrilldowndefaultcolumns.logsdrilldown.grafana.app" + }, + "spec": { + "group": "logsdrilldown.grafana.app", + "versions": [ + { + "name": "v1alpha1", + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": { + "properties": { + "spec": { + "properties": { + "records": { + "items": { + "properties": { + "columns": { + "items": { + "type": "string" + }, + "type": "array" + }, + "labels": { + "items": { + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": ["key", "value"], + "type": "object" + }, + "type": "array" + } + }, + "required": ["columns", "labels"], + "type": "object" + }, + "type": "array" + } + }, + "required": ["records"], + "type": "object" + }, + "status": { + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "x-kubernetes-preserve-unknown-fields": true + }, + "operatorStates": { + "additionalProperties": { + "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", + "x-kubernetes-preserve-unknown-fields": true + }, + "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" + }, + "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" + } + }, + "required": ["spec"], + "type": "object" + } + }, + "subresources": { + "status": {} + } + } + ], + "names": { + "kind": "LogsDrilldownDefaultColumns", + "plural": "logsdrilldowndefaultcolumns" + }, + "scope": "Namespaced" + } +} diff --git a/apps/logsdrilldown/definitions/logsdrilldowndefaults.logsdrilldown.grafana.app.json b/apps/logsdrilldown/definitions/logsdrilldowndefaults.logsdrilldown.grafana.app.json new file mode 100644 index 00000000000..f2ab4b77e80 --- /dev/null +++ b/apps/logsdrilldown/definitions/logsdrilldowndefaults.logsdrilldown.grafana.app.json @@ -0,0 +1,92 @@ +{ + "kind": "CustomResourceDefinition", + "apiVersion": "apiextensions.k8s.io/v1", + "metadata": { + "name": "logsdrilldowndefaults.logsdrilldown.grafana.app" + }, + "spec": { + "group": "logsdrilldown.grafana.app", + "versions": [ + { + "name": "v1alpha1", + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": { + "properties": { + "spec": { + "properties": { + "defaultFields": { + "items": { + "type": "string" + }, + "type": "array" + }, + "interceptDismissed": { + "type": "boolean" + }, + "prettifyJSON": { + "type": "boolean" + }, + "wrapLogMessage": { + "type": "boolean" + } + }, + "required": ["defaultFields", "prettifyJSON", "wrapLogMessage", "interceptDismissed"], + "type": "object" + }, + "status": { + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "x-kubernetes-preserve-unknown-fields": true + }, + "operatorStates": { + "additionalProperties": { + "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", + "x-kubernetes-preserve-unknown-fields": true + }, + "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" + }, + "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" + } + }, + "required": ["spec"], + "type": "object" + } + }, + "subresources": { + "status": {} + } + } + ], + "names": { + "kind": "LogsDrilldownDefaults", + "plural": "logsdrilldowndefaults" + }, + "scope": "Namespaced" + } +} diff --git a/apps/logsdrilldown/kinds/logsdrilldown.cue b/apps/logsdrilldown/kinds/logsdrilldown.cue index 4a560ae3c6e..d2752103820 100644 --- a/apps/logsdrilldown/kinds/logsdrilldown.cue +++ b/apps/logsdrilldown/kinds/logsdrilldown.cue @@ -1,5 +1,9 @@ package kinds +import ( + "github.com/grafana/grafana/apps/logsdrilldown/kinds/v0alpha1" +) + LogsDrilldownSpecv1alpha1: { defaultFields: [...string] | *[] prettifyJSON: bool @@ -21,3 +25,12 @@ logsdrilldownDefaultsv1alpha1: { spec: LogsDrilldownSpecv1alpha1 } } + +// Default columns API +logsdrilldownDefaultColumnsv0alpha1: { + kind: "LogsDrilldownDefaultColumns" + pluralName: "LogsDrilldownDefaultColumns" + schema: { + spec: v0alpha1.LogsDefaultColumns + } +} diff --git a/apps/logsdrilldown/kinds/manifest.cue b/apps/logsdrilldown/kinds/manifest.cue index e2f2fb236b1..ab717de6a92 100644 --- a/apps/logsdrilldown/kinds/manifest.cue +++ b/apps/logsdrilldown/kinds/manifest.cue @@ -35,12 +35,12 @@ manifest: { // It includes kinds which the v1alpha1 API serves, and (future) custom routes served globally from the v1alpha1 version. v1alpha1: { // kinds is the list of kinds served by this version - kinds: [logsdrilldownv1alpha1, logsdrilldownDefaultsv1alpha1] + kinds: [logsdrilldownv1alpha1, logsdrilldownDefaultsv1alpha1, logsdrilldownDefaultColumnsv0alpha1] // [OPTIONAL] // served indicates whether this particular version is served by the API server. // served should be set to false before a version is removed from the manifest entirely. // served defaults to true if not present. - served: true + served: true // [OPTIONAL] // Codegen is a trait that tells the grafana-app-sdk, or other code generation tooling, how to process this kind. // If not present, default values within the codegen trait are used. @@ -64,4 +64,4 @@ v1alpha1: { enabled: true } } -} \ No newline at end of file +} diff --git a/apps/logsdrilldown/kinds/v0alpha1/defaultcolumns.cue b/apps/logsdrilldown/kinds/v0alpha1/defaultcolumns.cue new file mode 100644 index 00000000000..123d5129c15 --- /dev/null +++ b/apps/logsdrilldown/kinds/v0alpha1/defaultcolumns.cue @@ -0,0 +1,19 @@ +package v0alpha1 + +#LogsDefaultColumnsLabel: { + key: string + value: string +} + +#LogsDefaultColumnsLabels: [...#LogsDefaultColumnsLabel] + +#LogsDefaultColumnsRecord: { + columns: [...string] + labels: #LogsDefaultColumnsLabels +} + +#LogsDefaultColumnsRecords: [...#LogsDefaultColumnsRecord] + +LogsDefaultColumns: { + records: #LogsDefaultColumnsRecords +} diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go index 4ede8bb4ee6..50bdc230cd1 100644 --- a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go @@ -25,6 +25,13 @@ type LogsDrilldown struct { Status LogsDrilldownStatus `json:"status" yaml:"status"` } +func NewLogsDrilldown() *LogsDrilldown { + return &LogsDrilldown{ + Spec: *NewLogsDrilldownSpec(), + Status: *NewLogsDrilldownStatus(), + } +} + func (o *LogsDrilldown) GetSpec() any { return o.Spec } diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_schema_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_schema_gen.go index e6864c965a7..8e88e870346 100644 --- a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_schema_gen.go +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldown_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaLogsDrilldown = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", &LogsDrilldown{}, &LogsDrilldownList{}, resource.WithKind("LogsDrilldown"), + schemaLogsDrilldown = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", NewLogsDrilldown(), &LogsDrilldownList{}, resource.WithKind("LogsDrilldown"), resource.WithPlural("logsdrilldowns"), resource.WithScope(resource.NamespacedScope)) kindLogsDrilldown = resource.Kind{ Schema: schemaLogsDrilldown, diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_client_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_client_gen.go new file mode 100644 index 00000000000..b5d573bc1dc --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_client_gen.go @@ -0,0 +1,99 @@ +package v1alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LogsDrilldownDefaultColumnsClient struct { + client *resource.TypedClient[*LogsDrilldownDefaultColumns, *LogsDrilldownDefaultColumnsList] +} + +func NewLogsDrilldownDefaultColumnsClient(client resource.Client) *LogsDrilldownDefaultColumnsClient { + return &LogsDrilldownDefaultColumnsClient{ + client: resource.NewTypedClient[*LogsDrilldownDefaultColumns, *LogsDrilldownDefaultColumnsList](client, LogsDrilldownDefaultColumnsKind()), + } +} + +func NewLogsDrilldownDefaultColumnsClientFromGenerator(generator resource.ClientGenerator) (*LogsDrilldownDefaultColumnsClient, error) { + c, err := generator.ClientFor(LogsDrilldownDefaultColumnsKind()) + if err != nil { + return nil, err + } + return NewLogsDrilldownDefaultColumnsClient(c), nil +} + +func (c *LogsDrilldownDefaultColumnsClient) Get(ctx context.Context, identifier resource.Identifier) (*LogsDrilldownDefaultColumns, error) { + return c.client.Get(ctx, identifier) +} + +func (c *LogsDrilldownDefaultColumnsClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultColumnsList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultColumnsList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *LogsDrilldownDefaultColumnsClient) Create(ctx context.Context, obj *LogsDrilldownDefaultColumns, opts resource.CreateOptions) (*LogsDrilldownDefaultColumns, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = LogsDrilldownDefaultColumnsKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) Update(ctx context.Context, obj *LogsDrilldownDefaultColumns, opts resource.UpdateOptions) (*LogsDrilldownDefaultColumns, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*LogsDrilldownDefaultColumns, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus LogsDrilldownDefaultColumnsStatus, opts resource.UpdateOptions) (*LogsDrilldownDefaultColumns, error) { + return c.client.Update(ctx, &LogsDrilldownDefaultColumns{ + TypeMeta: metav1.TypeMeta{ + Kind: LogsDrilldownDefaultColumnsKind().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 *LogsDrilldownDefaultColumnsClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_codec_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_codec_gen.go new file mode 100644 index 00000000000..311d2f02683 --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// LogsDrilldownDefaultColumnsJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type LogsDrilldownDefaultColumnsJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*LogsDrilldownDefaultColumnsJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*LogsDrilldownDefaultColumnsJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &LogsDrilldownDefaultColumnsJSONCodec{} diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_metadata_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_metadata_gen.go new file mode 100644 index 00000000000..a4bb052fe25 --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type LogsDrilldownDefaultColumnsMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewLogsDrilldownDefaultColumnsMetadata creates a new LogsDrilldownDefaultColumnsMetadata object. +func NewLogsDrilldownDefaultColumnsMetadata() *LogsDrilldownDefaultColumnsMetadata { + return &LogsDrilldownDefaultColumnsMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_object_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_object_gen.go new file mode 100644 index 00000000000..4340a27714e --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_object_gen.go @@ -0,0 +1,326 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumns struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the LogsDrilldownDefaultColumns + Spec LogsDrilldownDefaultColumnsSpec `json:"spec" yaml:"spec"` + + Status LogsDrilldownDefaultColumnsStatus `json:"status" yaml:"status"` +} + +func NewLogsDrilldownDefaultColumns() *LogsDrilldownDefaultColumns { + return &LogsDrilldownDefaultColumns{ + Spec: *NewLogsDrilldownDefaultColumnsSpec(), + Status: *NewLogsDrilldownDefaultColumnsStatus(), + } +} + +func (o *LogsDrilldownDefaultColumns) GetSpec() any { + return o.Spec +} + +func (o *LogsDrilldownDefaultColumns) SetSpec(spec any) error { + cast, ok := spec.(LogsDrilldownDefaultColumnsSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *LogsDrilldownDefaultColumns) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *LogsDrilldownDefaultColumns) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *LogsDrilldownDefaultColumns) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(LogsDrilldownDefaultColumnsStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type LogsDrilldownDefaultColumnsStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *LogsDrilldownDefaultColumns) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *LogsDrilldownDefaultColumns) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *LogsDrilldownDefaultColumns) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *LogsDrilldownDefaultColumns) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *LogsDrilldownDefaultColumns) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *LogsDrilldownDefaultColumns) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *LogsDrilldownDefaultColumns) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *LogsDrilldownDefaultColumns) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *LogsDrilldownDefaultColumns) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *LogsDrilldownDefaultColumns) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *LogsDrilldownDefaultColumns) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *LogsDrilldownDefaultColumns) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownDefaultColumns) DeepCopy() *LogsDrilldownDefaultColumns { + cpy := &LogsDrilldownDefaultColumns{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownDefaultColumns) DeepCopyInto(dst *LogsDrilldownDefaultColumns) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + 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 +var _ resource.Object = &LogsDrilldownDefaultColumns{} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []LogsDrilldownDefaultColumns `json:"items" yaml:"items"` +} + +func (o *LogsDrilldownDefaultColumnsList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownDefaultColumnsList) Copy() resource.ListObject { + cpy := &LogsDrilldownDefaultColumnsList{ + TypeMeta: o.TypeMeta, + Items: make([]LogsDrilldownDefaultColumns, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*LogsDrilldownDefaultColumns); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *LogsDrilldownDefaultColumnsList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *LogsDrilldownDefaultColumnsList) SetItems(items []resource.Object) { + o.Items = make([]LogsDrilldownDefaultColumns, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*LogsDrilldownDefaultColumns) + } +} + +func (o *LogsDrilldownDefaultColumnsList) DeepCopy() *LogsDrilldownDefaultColumnsList { + cpy := &LogsDrilldownDefaultColumnsList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownDefaultColumnsList) DeepCopyInto(dst *LogsDrilldownDefaultColumnsList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &LogsDrilldownDefaultColumnsList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *LogsDrilldownDefaultColumnsSpec) DeepCopy() *LogsDrilldownDefaultColumnsSpec { + cpy := &LogsDrilldownDefaultColumnsSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *LogsDrilldownDefaultColumnsSpec) DeepCopyInto(dst *LogsDrilldownDefaultColumnsSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of LogsDrilldownDefaultColumnsStatus +func (s *LogsDrilldownDefaultColumnsStatus) DeepCopy() *LogsDrilldownDefaultColumnsStatus { + cpy := &LogsDrilldownDefaultColumnsStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies LogsDrilldownDefaultColumnsStatus into another LogsDrilldownDefaultColumnsStatus object +func (s *LogsDrilldownDefaultColumnsStatus) DeepCopyInto(dst *LogsDrilldownDefaultColumnsStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_schema_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_schema_gen.go new file mode 100644 index 00000000000..cc5363e16bb --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaLogsDrilldownDefaultColumns = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", NewLogsDrilldownDefaultColumns(), &LogsDrilldownDefaultColumnsList{}, resource.WithKind("LogsDrilldownDefaultColumns"), + resource.WithPlural("logsdrilldowndefaultcolumns"), resource.WithScope(resource.NamespacedScope)) + kindLogsDrilldownDefaultColumns = resource.Kind{ + Schema: schemaLogsDrilldownDefaultColumns, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &LogsDrilldownDefaultColumnsJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func LogsDrilldownDefaultColumnsKind() resource.Kind { + return kindLogsDrilldownDefaultColumns +} + +// Schema returns a resource.SimpleSchema representation of LogsDrilldownDefaultColumns +func LogsDrilldownDefaultColumnsSchema() *resource.SimpleSchema { + return schemaLogsDrilldownDefaultColumns +} + +// Interface compliance checks +var _ resource.Schema = kindLogsDrilldownDefaultColumns diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_spec_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_spec_gen.go new file mode 100644 index 00000000000..ce12ebb0761 --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_spec_gen.go @@ -0,0 +1,43 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsLogsDefaultColumnsRecords []LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord struct { + Columns []string `json:"columns"` + Labels LogsDrilldownDefaultColumnsLogsDefaultColumnsLabels `json:"labels"` +} + +// NewLogsDrilldownDefaultColumnsLogsDefaultColumnsRecord creates a new LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord object. +func NewLogsDrilldownDefaultColumnsLogsDefaultColumnsRecord() *LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord { + return &LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord{ + Columns: []string{}, + } +} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsLogsDefaultColumnsLabels []LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// NewLogsDrilldownDefaultColumnsLogsDefaultColumnsLabel creates a new LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel object. +func NewLogsDrilldownDefaultColumnsLogsDefaultColumnsLabel() *LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel { + return &LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel{} +} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsSpec struct { + Records LogsDrilldownDefaultColumnsLogsDefaultColumnsRecords `json:"records"` +} + +// NewLogsDrilldownDefaultColumnsSpec creates a new LogsDrilldownDefaultColumnsSpec object. +func NewLogsDrilldownDefaultColumnsSpec() *LogsDrilldownDefaultColumnsSpec { + return &LogsDrilldownDefaultColumnsSpec{} +} diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_status_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_status_gen.go new file mode 100644 index 00000000000..c2183832095 --- /dev/null +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaultcolumns_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsstatusOperatorState 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 LogsDrilldownDefaultColumnsStatusOperatorStateState `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"` +} + +// NewLogsDrilldownDefaultColumnsstatusOperatorState creates a new LogsDrilldownDefaultColumnsstatusOperatorState object. +func NewLogsDrilldownDefaultColumnsstatusOperatorState() *LogsDrilldownDefaultColumnsstatusOperatorState { + return &LogsDrilldownDefaultColumnsstatusOperatorState{} +} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsStatus 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]LogsDrilldownDefaultColumnsstatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewLogsDrilldownDefaultColumnsStatus creates a new LogsDrilldownDefaultColumnsStatus object. +func NewLogsDrilldownDefaultColumnsStatus() *LogsDrilldownDefaultColumnsStatus { + return &LogsDrilldownDefaultColumnsStatus{} +} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsStatusOperatorStateState string + +const ( + LogsDrilldownDefaultColumnsStatusOperatorStateStateSuccess LogsDrilldownDefaultColumnsStatusOperatorStateState = "success" + LogsDrilldownDefaultColumnsStatusOperatorStateStateInProgress LogsDrilldownDefaultColumnsStatusOperatorStateState = "in_progress" + LogsDrilldownDefaultColumnsStatusOperatorStateStateFailed LogsDrilldownDefaultColumnsStatusOperatorStateState = "failed" +) diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_object_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_object_gen.go index 8dfb90a1bfb..ff5ce10adce 100644 --- a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_object_gen.go +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_object_gen.go @@ -25,6 +25,13 @@ type LogsDrilldownDefaults struct { Status LogsDrilldownDefaultsStatus `json:"status" yaml:"status"` } +func NewLogsDrilldownDefaults() *LogsDrilldownDefaults { + return &LogsDrilldownDefaults{ + Spec: *NewLogsDrilldownDefaultsSpec(), + Status: *NewLogsDrilldownDefaultsStatus(), + } +} + func (o *LogsDrilldownDefaults) GetSpec() any { return o.Spec } diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_schema_gen.go b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_schema_gen.go index 20e952ce021..f488c319db2 100644 --- a/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_schema_gen.go +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown/v1alpha1/logsdrilldowndefaults_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaLogsDrilldownDefaults = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", &LogsDrilldownDefaults{}, &LogsDrilldownDefaultsList{}, resource.WithKind("LogsDrilldownDefaults"), + schemaLogsDrilldownDefaults = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", NewLogsDrilldownDefaults(), &LogsDrilldownDefaultsList{}, resource.WithKind("LogsDrilldownDefaults"), resource.WithPlural("logsdrilldowndefaults"), resource.WithScope(resource.NamespacedScope)) kindLogsDrilldownDefaults = resource.Kind{ Schema: schemaLogsDrilldownDefaults, diff --git a/apps/logsdrilldown/pkg/apis/logsdrilldown_manifest.go b/apps/logsdrilldown/pkg/apis/logsdrilldown_manifest.go index ef0d5801511..2350b924dda 100644 --- a/apps/logsdrilldown/pkg/apis/logsdrilldown_manifest.go +++ b/apps/logsdrilldown/pkg/apis/logsdrilldown_manifest.go @@ -20,12 +20,15 @@ import ( ) var ( - rawSchemaLogsDrilldownv1alpha1 = []byte(`{"LogsDrilldown":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"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"},"spec":{"additionalProperties":false,"properties":{"defaultFields":{"items":{"type":"string"},"type":"array"},"interceptDismissed":{"type":"boolean"},"prettifyJSON":{"type":"boolean"},"wrapLogMessage":{"type":"boolean"}},"required":["defaultFields","prettifyJSON","wrapLogMessage","interceptDismissed"],"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"}}`) - versionSchemaLogsDrilldownv1alpha1 app.VersionSchema - _ = json.Unmarshal(rawSchemaLogsDrilldownv1alpha1, &versionSchemaLogsDrilldownv1alpha1) - rawSchemaLogsDrilldownDefaultsv1alpha1 = []byte(`{"LogsDrilldownDefaults":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"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"},"spec":{"additionalProperties":false,"properties":{"defaultFields":{"items":{"type":"string"},"type":"array"},"interceptDismissed":{"type":"boolean"},"prettifyJSON":{"type":"boolean"},"wrapLogMessage":{"type":"boolean"}},"required":["defaultFields","prettifyJSON","wrapLogMessage","interceptDismissed"],"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"}}`) - versionSchemaLogsDrilldownDefaultsv1alpha1 app.VersionSchema - _ = json.Unmarshal(rawSchemaLogsDrilldownDefaultsv1alpha1, &versionSchemaLogsDrilldownDefaultsv1alpha1) + rawSchemaLogsDrilldownv1alpha1 = []byte(`{"LogsDrilldown":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"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"},"spec":{"additionalProperties":false,"properties":{"defaultFields":{"items":{"type":"string"},"type":"array"},"interceptDismissed":{"type":"boolean"},"prettifyJSON":{"type":"boolean"},"wrapLogMessage":{"type":"boolean"}},"required":["defaultFields","prettifyJSON","wrapLogMessage","interceptDismissed"],"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"}}`) + versionSchemaLogsDrilldownv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaLogsDrilldownv1alpha1, &versionSchemaLogsDrilldownv1alpha1) + rawSchemaLogsDrilldownDefaultsv1alpha1 = []byte(`{"LogsDrilldownDefaults":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"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"},"spec":{"additionalProperties":false,"properties":{"defaultFields":{"items":{"type":"string"},"type":"array"},"interceptDismissed":{"type":"boolean"},"prettifyJSON":{"type":"boolean"},"wrapLogMessage":{"type":"boolean"}},"required":["defaultFields","prettifyJSON","wrapLogMessage","interceptDismissed"],"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"}}`) + versionSchemaLogsDrilldownDefaultsv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaLogsDrilldownDefaultsv1alpha1, &versionSchemaLogsDrilldownDefaultsv1alpha1) + rawSchemaLogsDrilldownDefaultColumnsv1alpha1 = []byte(`{"LogsDefaultColumnsLabel":{"additionalProperties":false,"properties":{"key":{"type":"string"},"value":{"type":"string"}},"required":["key","value"],"type":"object"},"LogsDefaultColumnsLabels":{"items":{"$ref":"#/components/schemas/LogsDefaultColumnsLabel"},"type":"array"},"LogsDefaultColumnsRecord":{"additionalProperties":false,"properties":{"columns":{"items":{"type":"string"},"type":"array"},"labels":{"$ref":"#/components/schemas/LogsDefaultColumnsLabels"}},"required":["columns","labels"],"type":"object"},"LogsDefaultColumnsRecords":{"items":{"$ref":"#/components/schemas/LogsDefaultColumnsRecord"},"type":"array"},"LogsDrilldownDefaultColumns":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"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"},"spec":{"additionalProperties":false,"properties":{"records":{"$ref":"#/components/schemas/LogsDefaultColumnsRecords"}},"required":["records"],"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"}}`) + versionSchemaLogsDrilldownDefaultColumnsv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaLogsDrilldownDefaultColumnsv1alpha1, &versionSchemaLogsDrilldownDefaultColumnsv1alpha1) ) var appManifestData = app.ManifestData{ @@ -52,6 +55,14 @@ var appManifestData = app.ManifestData{ Conversion: false, Schema: &versionSchemaLogsDrilldownDefaultsv1alpha1, }, + + { + Kind: "LogsDrilldownDefaultColumns", + Plural: "LogsDrilldownDefaultColumns", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaLogsDrilldownDefaultColumnsv1alpha1, + }, }, Routes: app.ManifestVersionRoutes{ Namespaced: map[string]spec3.PathProps{}, @@ -71,8 +82,9 @@ func RemoteManifest() app.Manifest { } var kindVersionToGoType = map[string]resource.Kind{ - "LogsDrilldown/v1alpha1": v1alpha1.LogsDrilldownKind(), - "LogsDrilldownDefaults/v1alpha1": v1alpha1.LogsDrilldownDefaultsKind(), + "LogsDrilldown/v1alpha1": v1alpha1.LogsDrilldownKind(), + "LogsDrilldownDefaults/v1alpha1": v1alpha1.LogsDrilldownDefaultsKind(), + "LogsDrilldownDefaultColumns/v1alpha1": v1alpha1.LogsDrilldownDefaultColumnsKind(), } // ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. diff --git a/apps/logsdrilldown/pkg/app/app.go b/apps/logsdrilldown/pkg/app/app.go index 7fda1ecb99f..23260270207 100644 --- a/apps/logsdrilldown/pkg/app/app.go +++ b/apps/logsdrilldown/pkg/app/app.go @@ -31,6 +31,9 @@ func New(cfg app.Config) (app.App, error) { { Kind: logsdrilldownv1alpha1.LogsDrilldownDefaultsKind(), }, + { + Kind: logsdrilldownv1alpha1.LogsDrilldownDefaultColumnsKind(), + }, }, } diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/constants.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/constants.go new file mode 100644 index 00000000000..082bec7c874 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/constants.go @@ -0,0 +1,18 @@ +package v1alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "logsdrilldown.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v1alpha1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_client_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_client_gen.go new file mode 100644 index 00000000000..c133b65f45b --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_client_gen.go @@ -0,0 +1,99 @@ +package v1alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LogsDrilldownClient struct { + client *resource.TypedClient[*LogsDrilldown, *LogsDrilldownList] +} + +func NewLogsDrilldownClient(client resource.Client) *LogsDrilldownClient { + return &LogsDrilldownClient{ + client: resource.NewTypedClient[*LogsDrilldown, *LogsDrilldownList](client, Kind()), + } +} + +func NewLogsDrilldownClientFromGenerator(generator resource.ClientGenerator) (*LogsDrilldownClient, error) { + c, err := generator.ClientFor(Kind()) + if err != nil { + return nil, err + } + return NewLogsDrilldownClient(c), nil +} + +func (c *LogsDrilldownClient) Get(ctx context.Context, identifier resource.Identifier) (*LogsDrilldown, error) { + return c.client.Get(ctx, identifier) +} + +func (c *LogsDrilldownClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *LogsDrilldownClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *LogsDrilldownClient) Create(ctx context.Context, obj *LogsDrilldown, opts resource.CreateOptions) (*LogsDrilldown, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = Kind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *LogsDrilldownClient) Update(ctx context.Context, obj *LogsDrilldown, opts resource.UpdateOptions) (*LogsDrilldown, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *LogsDrilldownClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*LogsDrilldown, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *LogsDrilldownClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus Status, opts resource.UpdateOptions) (*LogsDrilldown, error) { + return c.client.Update(ctx, &LogsDrilldown{ + TypeMeta: metav1.TypeMeta{ + Kind: Kind().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 *LogsDrilldownClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_codec_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_codec_gen.go new file mode 100644 index 00000000000..bb458caeb88 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// JSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type JSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*JSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*JSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &JSONCodec{} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_metadata_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_metadata_gen.go new file mode 100644 index 00000000000..cb7233b22ab --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type Metadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewMetadata creates a new Metadata object. +func NewMetadata() *Metadata { + return &Metadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go new file mode 100644 index 00000000000..5d40a873e6b --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type LogsDrilldown struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the LogsDrilldown + Spec Spec `json:"spec" yaml:"spec"` + + Status Status `json:"status" yaml:"status"` +} + +func (o *LogsDrilldown) GetSpec() any { + return o.Spec +} + +func (o *LogsDrilldown) SetSpec(spec any) error { + cast, ok := spec.(Spec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *LogsDrilldown) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *LogsDrilldown) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *LogsDrilldown) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(Status) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type Status", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *LogsDrilldown) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *LogsDrilldown) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *LogsDrilldown) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *LogsDrilldown) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *LogsDrilldown) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *LogsDrilldown) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *LogsDrilldown) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *LogsDrilldown) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *LogsDrilldown) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *LogsDrilldown) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *LogsDrilldown) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *LogsDrilldown) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldown) DeepCopy() *LogsDrilldown { + cpy := &LogsDrilldown{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldown) DeepCopyInto(dst *LogsDrilldown) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + 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 +var _ resource.Object = &LogsDrilldown{} + +// +k8s:openapi-gen=true +type LogsDrilldownList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []LogsDrilldown `json:"items" yaml:"items"` +} + +func (o *LogsDrilldownList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownList) Copy() resource.ListObject { + cpy := &LogsDrilldownList{ + TypeMeta: o.TypeMeta, + Items: make([]LogsDrilldown, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*LogsDrilldown); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *LogsDrilldownList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *LogsDrilldownList) SetItems(items []resource.Object) { + o.Items = make([]LogsDrilldown, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*LogsDrilldown) + } +} + +func (o *LogsDrilldownList) DeepCopy() *LogsDrilldownList { + cpy := &LogsDrilldownList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownList) DeepCopyInto(dst *LogsDrilldownList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &LogsDrilldownList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *Spec) DeepCopy() *Spec { + cpy := &Spec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *Spec) DeepCopyInto(dst *Spec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of Status +func (s *Status) DeepCopy() *Status { + cpy := &Status{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Status into another Status object +func (s *Status) DeepCopyInto(dst *Status) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_schema_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_schema_gen.go new file mode 100644 index 00000000000..942794416e8 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaLogsDrilldown = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", &LogsDrilldown{}, &LogsDrilldownList{}, resource.WithKind("LogsDrilldown"), + resource.WithPlural("logsdrilldowns"), resource.WithScope(resource.NamespacedScope)) + kindLogsDrilldown = resource.Kind{ + Schema: schemaLogsDrilldown, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &JSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func Kind() resource.Kind { + return kindLogsDrilldown +} + +// Schema returns a resource.SimpleSchema representation of LogsDrilldown +func Schema() *resource.SimpleSchema { + return schemaLogsDrilldown +} + +// Interface compliance checks +var _ resource.Schema = kindLogsDrilldown diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_spec_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_spec_gen.go new file mode 100644 index 00000000000..faff5c108dd --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_spec_gen.go @@ -0,0 +1,18 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type Spec struct { + DefaultFields []string `json:"defaultFields"` + PrettifyJSON bool `json:"prettifyJSON"` + WrapLogMessage bool `json:"wrapLogMessage"` + InterceptDismissed bool `json:"interceptDismissed"` +} + +// NewSpec creates a new Spec object. +func NewSpec() *Spec { + return &Spec{ + DefaultFields: []string{}, + } +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_status_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_status_gen.go new file mode 100644 index 00000000000..9b227b00f44 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1/logsdrilldown_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type StatusOperatorState 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 StatusOperatorStateState `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"` +} + +// NewStatusOperatorState creates a new StatusOperatorState object. +func NewStatusOperatorState() *StatusOperatorState { + return &StatusOperatorState{} +} + +// +k8s:openapi-gen=true +type Status 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]StatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewStatus creates a new Status object. +func NewStatus() *Status { + return &Status{} +} + +// +k8s:openapi-gen=true +type StatusOperatorStateState string + +const ( + StatusOperatorStateStateSuccess StatusOperatorStateState = "success" + StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress" + StatusOperatorStateStateFailed StatusOperatorStateState = "failed" +) diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/constants.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/constants.go new file mode 100644 index 00000000000..082bec7c874 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/constants.go @@ -0,0 +1,18 @@ +package v1alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "logsdrilldown.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v1alpha1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_client_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_client_gen.go new file mode 100644 index 00000000000..b66471eb4ba --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_client_gen.go @@ -0,0 +1,99 @@ +package v1alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LogsDrilldownDefaultColumnsClient struct { + client *resource.TypedClient[*LogsDrilldownDefaultColumns, *LogsDrilldownDefaultColumnsList] +} + +func NewLogsDrilldownDefaultColumnsClient(client resource.Client) *LogsDrilldownDefaultColumnsClient { + return &LogsDrilldownDefaultColumnsClient{ + client: resource.NewTypedClient[*LogsDrilldownDefaultColumns, *LogsDrilldownDefaultColumnsList](client, Kind()), + } +} + +func NewLogsDrilldownDefaultColumnsClientFromGenerator(generator resource.ClientGenerator) (*LogsDrilldownDefaultColumnsClient, error) { + c, err := generator.ClientFor(Kind()) + if err != nil { + return nil, err + } + return NewLogsDrilldownDefaultColumnsClient(c), nil +} + +func (c *LogsDrilldownDefaultColumnsClient) Get(ctx context.Context, identifier resource.Identifier) (*LogsDrilldownDefaultColumns, error) { + return c.client.Get(ctx, identifier) +} + +func (c *LogsDrilldownDefaultColumnsClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultColumnsList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultColumnsList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *LogsDrilldownDefaultColumnsClient) Create(ctx context.Context, obj *LogsDrilldownDefaultColumns, opts resource.CreateOptions) (*LogsDrilldownDefaultColumns, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = Kind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) Update(ctx context.Context, obj *LogsDrilldownDefaultColumns, opts resource.UpdateOptions) (*LogsDrilldownDefaultColumns, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*LogsDrilldownDefaultColumns, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *LogsDrilldownDefaultColumnsClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus Status, opts resource.UpdateOptions) (*LogsDrilldownDefaultColumns, error) { + return c.client.Update(ctx, &LogsDrilldownDefaultColumns{ + TypeMeta: metav1.TypeMeta{ + Kind: Kind().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 *LogsDrilldownDefaultColumnsClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_codec_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_codec_gen.go new file mode 100644 index 00000000000..bb458caeb88 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// JSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type JSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*JSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*JSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &JSONCodec{} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_metadata_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_metadata_gen.go new file mode 100644 index 00000000000..cb7233b22ab --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type Metadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewMetadata creates a new Metadata object. +func NewMetadata() *Metadata { + return &Metadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_object_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_object_gen.go new file mode 100644 index 00000000000..3173c28330e --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumns struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the LogsDrilldownDefaultColumns + Spec Spec `json:"spec" yaml:"spec"` + + Status Status `json:"status" yaml:"status"` +} + +func (o *LogsDrilldownDefaultColumns) GetSpec() any { + return o.Spec +} + +func (o *LogsDrilldownDefaultColumns) SetSpec(spec any) error { + cast, ok := spec.(Spec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *LogsDrilldownDefaultColumns) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *LogsDrilldownDefaultColumns) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *LogsDrilldownDefaultColumns) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(Status) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type Status", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *LogsDrilldownDefaultColumns) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *LogsDrilldownDefaultColumns) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *LogsDrilldownDefaultColumns) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *LogsDrilldownDefaultColumns) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *LogsDrilldownDefaultColumns) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *LogsDrilldownDefaultColumns) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *LogsDrilldownDefaultColumns) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *LogsDrilldownDefaultColumns) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *LogsDrilldownDefaultColumns) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *LogsDrilldownDefaultColumns) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *LogsDrilldownDefaultColumns) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *LogsDrilldownDefaultColumns) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownDefaultColumns) DeepCopy() *LogsDrilldownDefaultColumns { + cpy := &LogsDrilldownDefaultColumns{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownDefaultColumns) DeepCopyInto(dst *LogsDrilldownDefaultColumns) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + 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 +var _ resource.Object = &LogsDrilldownDefaultColumns{} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultColumnsList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []LogsDrilldownDefaultColumns `json:"items" yaml:"items"` +} + +func (o *LogsDrilldownDefaultColumnsList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownDefaultColumnsList) Copy() resource.ListObject { + cpy := &LogsDrilldownDefaultColumnsList{ + TypeMeta: o.TypeMeta, + Items: make([]LogsDrilldownDefaultColumns, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*LogsDrilldownDefaultColumns); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *LogsDrilldownDefaultColumnsList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *LogsDrilldownDefaultColumnsList) SetItems(items []resource.Object) { + o.Items = make([]LogsDrilldownDefaultColumns, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*LogsDrilldownDefaultColumns) + } +} + +func (o *LogsDrilldownDefaultColumnsList) DeepCopy() *LogsDrilldownDefaultColumnsList { + cpy := &LogsDrilldownDefaultColumnsList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownDefaultColumnsList) DeepCopyInto(dst *LogsDrilldownDefaultColumnsList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &LogsDrilldownDefaultColumnsList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *Spec) DeepCopy() *Spec { + cpy := &Spec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *Spec) DeepCopyInto(dst *Spec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of Status +func (s *Status) DeepCopy() *Status { + cpy := &Status{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Status into another Status object +func (s *Status) DeepCopyInto(dst *Status) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_schema_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_schema_gen.go new file mode 100644 index 00000000000..b50be391fc7 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaLogsDrilldownDefaultColumns = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", &LogsDrilldownDefaultColumns{}, &LogsDrilldownDefaultColumnsList{}, resource.WithKind("LogsDrilldownDefaultColumns"), + resource.WithPlural("logsdrilldowndefaultcolumns"), resource.WithScope(resource.NamespacedScope)) + kindLogsDrilldownDefaultColumns = resource.Kind{ + Schema: schemaLogsDrilldownDefaultColumns, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &JSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func Kind() resource.Kind { + return kindLogsDrilldownDefaultColumns +} + +// Schema returns a resource.SimpleSchema representation of LogsDrilldownDefaultColumns +func Schema() *resource.SimpleSchema { + return schemaLogsDrilldownDefaultColumns +} + +// Interface compliance checks +var _ resource.Schema = kindLogsDrilldownDefaultColumns diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_spec_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_spec_gen.go new file mode 100644 index 00000000000..d9cd977aeb9 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_spec_gen.go @@ -0,0 +1,43 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type LogsDefaultColumnsRecords []LogsDefaultColumnsRecord + +// +k8s:openapi-gen=true +type LogsDefaultColumnsRecord struct { + Columns []string `json:"columns"` + Labels LogsDefaultColumnsLabels `json:"labels"` +} + +// NewLogsDefaultColumnsRecord creates a new LogsDefaultColumnsRecord object. +func NewLogsDefaultColumnsRecord() *LogsDefaultColumnsRecord { + return &LogsDefaultColumnsRecord{ + Columns: []string{}, + } +} + +// +k8s:openapi-gen=true +type LogsDefaultColumnsLabels []LogsDefaultColumnsLabel + +// +k8s:openapi-gen=true +type LogsDefaultColumnsLabel struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// NewLogsDefaultColumnsLabel creates a new LogsDefaultColumnsLabel object. +func NewLogsDefaultColumnsLabel() *LogsDefaultColumnsLabel { + return &LogsDefaultColumnsLabel{} +} + +// +k8s:openapi-gen=true +type Spec struct { + Records LogsDefaultColumnsRecords `json:"records"` +} + +// NewSpec creates a new Spec object. +func NewSpec() *Spec { + return &Spec{} +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_status_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_status_gen.go new file mode 100644 index 00000000000..9b227b00f44 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type StatusOperatorState 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 StatusOperatorStateState `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"` +} + +// NewStatusOperatorState creates a new StatusOperatorState object. +func NewStatusOperatorState() *StatusOperatorState { + return &StatusOperatorState{} +} + +// +k8s:openapi-gen=true +type Status 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]StatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewStatus creates a new Status object. +func NewStatus() *Status { + return &Status{} +} + +// +k8s:openapi-gen=true +type StatusOperatorStateState string + +const ( + StatusOperatorStateStateSuccess StatusOperatorStateState = "success" + StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress" + StatusOperatorStateStateFailed StatusOperatorStateState = "failed" +) diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/constants.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/constants.go new file mode 100644 index 00000000000..082bec7c874 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/constants.go @@ -0,0 +1,18 @@ +package v1alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "logsdrilldown.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v1alpha1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_client_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_client_gen.go new file mode 100644 index 00000000000..cc06a10b1e7 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_client_gen.go @@ -0,0 +1,99 @@ +package v1alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LogsDrilldownDefaultsClient struct { + client *resource.TypedClient[*LogsDrilldownDefaults, *LogsDrilldownDefaultsList] +} + +func NewLogsDrilldownDefaultsClient(client resource.Client) *LogsDrilldownDefaultsClient { + return &LogsDrilldownDefaultsClient{ + client: resource.NewTypedClient[*LogsDrilldownDefaults, *LogsDrilldownDefaultsList](client, Kind()), + } +} + +func NewLogsDrilldownDefaultsClientFromGenerator(generator resource.ClientGenerator) (*LogsDrilldownDefaultsClient, error) { + c, err := generator.ClientFor(Kind()) + if err != nil { + return nil, err + } + return NewLogsDrilldownDefaultsClient(c), nil +} + +func (c *LogsDrilldownDefaultsClient) Get(ctx context.Context, identifier resource.Identifier) (*LogsDrilldownDefaults, error) { + return c.client.Get(ctx, identifier) +} + +func (c *LogsDrilldownDefaultsClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultsList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *LogsDrilldownDefaultsClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultsList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *LogsDrilldownDefaultsClient) Create(ctx context.Context, obj *LogsDrilldownDefaults, opts resource.CreateOptions) (*LogsDrilldownDefaults, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = Kind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *LogsDrilldownDefaultsClient) Update(ctx context.Context, obj *LogsDrilldownDefaults, opts resource.UpdateOptions) (*LogsDrilldownDefaults, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *LogsDrilldownDefaultsClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*LogsDrilldownDefaults, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *LogsDrilldownDefaultsClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus Status, opts resource.UpdateOptions) (*LogsDrilldownDefaults, error) { + return c.client.Update(ctx, &LogsDrilldownDefaults{ + TypeMeta: metav1.TypeMeta{ + Kind: Kind().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 *LogsDrilldownDefaultsClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_codec_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_codec_gen.go new file mode 100644 index 00000000000..bb458caeb88 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// JSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type JSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*JSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*JSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &JSONCodec{} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_metadata_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_metadata_gen.go new file mode 100644 index 00000000000..cb7233b22ab --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type Metadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewMetadata creates a new Metadata object. +func NewMetadata() *Metadata { + return &Metadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_object_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_object_gen.go new file mode 100644 index 00000000000..d9354522dd7 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type LogsDrilldownDefaults struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the LogsDrilldownDefaults + Spec Spec `json:"spec" yaml:"spec"` + + Status Status `json:"status" yaml:"status"` +} + +func (o *LogsDrilldownDefaults) GetSpec() any { + return o.Spec +} + +func (o *LogsDrilldownDefaults) SetSpec(spec any) error { + cast, ok := spec.(Spec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *LogsDrilldownDefaults) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *LogsDrilldownDefaults) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *LogsDrilldownDefaults) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(Status) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type Status", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *LogsDrilldownDefaults) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *LogsDrilldownDefaults) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *LogsDrilldownDefaults) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *LogsDrilldownDefaults) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *LogsDrilldownDefaults) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *LogsDrilldownDefaults) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *LogsDrilldownDefaults) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *LogsDrilldownDefaults) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *LogsDrilldownDefaults) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *LogsDrilldownDefaults) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *LogsDrilldownDefaults) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *LogsDrilldownDefaults) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownDefaults) DeepCopy() *LogsDrilldownDefaults { + cpy := &LogsDrilldownDefaults{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownDefaults) DeepCopyInto(dst *LogsDrilldownDefaults) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + 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 +var _ resource.Object = &LogsDrilldownDefaults{} + +// +k8s:openapi-gen=true +type LogsDrilldownDefaultsList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []LogsDrilldownDefaults `json:"items" yaml:"items"` +} + +func (o *LogsDrilldownDefaultsList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *LogsDrilldownDefaultsList) Copy() resource.ListObject { + cpy := &LogsDrilldownDefaultsList{ + TypeMeta: o.TypeMeta, + Items: make([]LogsDrilldownDefaults, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*LogsDrilldownDefaults); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *LogsDrilldownDefaultsList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *LogsDrilldownDefaultsList) SetItems(items []resource.Object) { + o.Items = make([]LogsDrilldownDefaults, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*LogsDrilldownDefaults) + } +} + +func (o *LogsDrilldownDefaultsList) DeepCopy() *LogsDrilldownDefaultsList { + cpy := &LogsDrilldownDefaultsList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *LogsDrilldownDefaultsList) DeepCopyInto(dst *LogsDrilldownDefaultsList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &LogsDrilldownDefaultsList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *Spec) DeepCopy() *Spec { + cpy := &Spec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *Spec) DeepCopyInto(dst *Spec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of Status +func (s *Status) DeepCopy() *Status { + cpy := &Status{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Status into another Status object +func (s *Status) DeepCopyInto(dst *Status) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_schema_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_schema_gen.go new file mode 100644 index 00000000000..bda3e49377d --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaLogsDrilldownDefaults = resource.NewSimpleSchema("logsdrilldown.grafana.app", "v1alpha1", &LogsDrilldownDefaults{}, &LogsDrilldownDefaultsList{}, resource.WithKind("LogsDrilldownDefaults"), + resource.WithPlural("logsdrilldowndefaults"), resource.WithScope(resource.NamespacedScope)) + kindLogsDrilldownDefaults = resource.Kind{ + Schema: schemaLogsDrilldownDefaults, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &JSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func Kind() resource.Kind { + return kindLogsDrilldownDefaults +} + +// Schema returns a resource.SimpleSchema representation of LogsDrilldownDefaults +func Schema() *resource.SimpleSchema { + return schemaLogsDrilldownDefaults +} + +// Interface compliance checks +var _ resource.Schema = kindLogsDrilldownDefaults diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_spec_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_spec_gen.go new file mode 100644 index 00000000000..faff5c108dd --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_spec_gen.go @@ -0,0 +1,18 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type Spec struct { + DefaultFields []string `json:"defaultFields"` + PrettifyJSON bool `json:"prettifyJSON"` + WrapLogMessage bool `json:"wrapLogMessage"` + InterceptDismissed bool `json:"interceptDismissed"` +} + +// NewSpec creates a new Spec object. +func NewSpec() *Spec { + return &Spec{ + DefaultFields: []string{}, + } +} diff --git a/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_status_gen.go b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_status_gen.go new file mode 100644 index 00000000000..9b227b00f44 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1/logsdrilldowndefaults_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type StatusOperatorState 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 StatusOperatorStateState `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"` +} + +// NewStatusOperatorState creates a new StatusOperatorState object. +func NewStatusOperatorState() *StatusOperatorState { + return &StatusOperatorState{} +} + +// +k8s:openapi-gen=true +type Status 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]StatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewStatus creates a new Status object. +func NewStatus() *Status { + return &Status{} +} + +// +k8s:openapi-gen=true +type StatusOperatorStateState string + +const ( + StatusOperatorStateStateSuccess StatusOperatorStateState = "success" + StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress" + StatusOperatorStateStateFailed StatusOperatorStateState = "failed" +) diff --git a/apps/logsdrilldown/pkg/generated/manifestdata/logsdrilldown_manifest.go b/apps/logsdrilldown/pkg/generated/manifestdata/logsdrilldown_manifest.go new file mode 100644 index 00000000000..9deb5d5d3a1 --- /dev/null +++ b/apps/logsdrilldown/pkg/generated/manifestdata/logsdrilldown_manifest.go @@ -0,0 +1,150 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package manifestdata + +import ( + "encoding/json" + "fmt" + "strings" + + "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" + "k8s.io/kube-openapi/pkg/validation/spec" + + logsdrilldownv1alpha1 "github.com/grafana/grafana/apps/logsdrilldown/pkg/generated/logsdrilldown/v1alpha1" + logsdrilldowndefaultcolumnsv1alpha1 "github.com/grafana/grafana/apps/logsdrilldown/pkg/generated/logsdrilldowndefaultcolumns/v1alpha1" + logsdrilldowndefaultsv1alpha1 "github.com/grafana/grafana/apps/logsdrilldown/pkg/generated/logsdrilldowndefaults/v1alpha1" +) + +var ( + rawSchemaLogsDrilldownv1alpha1 = []byte(`{"LogsDrilldown":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"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"},"spec":{"additionalProperties":false,"properties":{"defaultFields":{"items":{"type":"string"},"type":"array"},"interceptDismissed":{"type":"boolean"},"prettifyJSON":{"type":"boolean"},"wrapLogMessage":{"type":"boolean"}},"required":["defaultFields","prettifyJSON","wrapLogMessage","interceptDismissed"],"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"}}`) + versionSchemaLogsDrilldownv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaLogsDrilldownv1alpha1, &versionSchemaLogsDrilldownv1alpha1) + rawSchemaLogsDrilldownDefaultsv1alpha1 = []byte(`{"LogsDrilldownDefaults":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"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"},"spec":{"additionalProperties":false,"properties":{"defaultFields":{"items":{"type":"string"},"type":"array"},"interceptDismissed":{"type":"boolean"},"prettifyJSON":{"type":"boolean"},"wrapLogMessage":{"type":"boolean"}},"required":["defaultFields","prettifyJSON","wrapLogMessage","interceptDismissed"],"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"}}`) + versionSchemaLogsDrilldownDefaultsv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaLogsDrilldownDefaultsv1alpha1, &versionSchemaLogsDrilldownDefaultsv1alpha1) + rawSchemaLogsDrilldownDefaultColumnsv1alpha1 = []byte(`{"LogsDefaultColumnsLabel":{"additionalProperties":false,"properties":{"key":{"type":"string"},"value":{"type":"string"}},"required":["key","value"],"type":"object"},"LogsDefaultColumnsLabels":{"items":{"$ref":"#/components/schemas/LogsDefaultColumnsLabel"},"type":"array"},"LogsDefaultColumnsRecord":{"additionalProperties":false,"properties":{"columns":{"items":{"type":"string"},"type":"array"},"labels":{"$ref":"#/components/schemas/LogsDefaultColumnsLabels"}},"required":["columns","labels"],"type":"object"},"LogsDefaultColumnsRecords":{"items":{"$ref":"#/components/schemas/LogsDefaultColumnsRecord"},"type":"array"},"LogsDrilldownDefaultColumns":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"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"},"spec":{"additionalProperties":false,"properties":{"records":{"$ref":"#/components/schemas/LogsDefaultColumnsRecords"}},"required":["records"],"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"}}`) + versionSchemaLogsDrilldownDefaultColumnsv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaLogsDrilldownDefaultColumnsv1alpha1, &versionSchemaLogsDrilldownDefaultColumnsv1alpha1) +) + +var appManifestData = app.ManifestData{ + AppName: "logsdrilldown", + Group: "logsdrilldown.grafana.app", + PreferredVersion: "v1alpha1", + Versions: []app.ManifestVersion{ + { + Name: "v1alpha1", + Served: true, + Kinds: []app.ManifestVersionKind{ + { + Kind: "LogsDrilldown", + Plural: "LogsDrilldowns", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaLogsDrilldownv1alpha1, + }, + + { + Kind: "LogsDrilldownDefaults", + Plural: "LogsDrilldownDefaults", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaLogsDrilldownDefaultsv1alpha1, + }, + + { + Kind: "LogsDrilldownDefaultColumns", + Plural: "LogsDrilldownDefaultColumns", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaLogsDrilldownDefaultColumnsv1alpha1, + }, + }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + Cluster: map[string]spec3.PathProps{}, + Schemas: map[string]spec.Schema{}, + }, + }, + }, +} + +func LocalManifest() app.Manifest { + return app.NewEmbeddedManifest(appManifestData) +} + +func RemoteManifest() app.Manifest { + return app.NewAPIServerManifest("logsdrilldown") +} + +var kindVersionToGoType = map[string]resource.Kind{ + "LogsDrilldown/v1alpha1": logsdrilldownv1alpha1.Kind(), + "LogsDrilldownDefaults/v1alpha1": logsdrilldowndefaultsv1alpha1.Kind(), + "LogsDrilldownDefaultColumns/v1alpha1": logsdrilldowndefaultcolumnsv1alpha1.Kind(), +} + +// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. +// If there is no association for the provided Kind and Version, exists will return false. +func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { + goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] + return goType, exists +} + +var customRouteToGoResponseType = map[string]any{} + +// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. +// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. +// If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) +func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoParamsType = map[string]runtime.Object{} + +func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +type GoTypeAssociator struct{} + +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + +func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { + return ManifestGoTypeAssociator(kind, version) +} +func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteResponsesAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { + return ManifestCustomRouteQueryAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_object_gen.ts b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_object_gen.ts new file mode 100644 index 00000000000..f7ba7b0f223 --- /dev/null +++ b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/logsdrilldowndefaultcolumns_object_gen.ts @@ -0,0 +1,49 @@ +/* + * 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; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface LogsDrilldownDefaultColumns { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.metadata.gen.ts b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.spec.gen.ts b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..fde99894776 --- /dev/null +++ b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.spec.gen.ts @@ -0,0 +1,38 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export type LogsDefaultColumnsRecords = LogsDefaultColumnsRecord[]; + +export const defaultLogsDefaultColumnsRecords = (): LogsDefaultColumnsRecords => ([]); + +export interface LogsDefaultColumnsRecord { + columns: string[]; + labels: LogsDefaultColumnsLabels; +} + +export const defaultLogsDefaultColumnsRecord = (): LogsDefaultColumnsRecord => ({ + columns: [], + labels: defaultLogsDefaultColumnsLabels(), +}); + +export type LogsDefaultColumnsLabels = LogsDefaultColumnsLabel[]; + +export const defaultLogsDefaultColumnsLabels = (): LogsDefaultColumnsLabels => ([]); + +export interface LogsDefaultColumnsLabel { + key: string; + value: string; +} + +export const defaultLogsDefaultColumnsLabel = (): LogsDefaultColumnsLabel => ({ + key: "", + value: "", +}); + +export interface Spec { + records: LogsDefaultColumnsRecords; +} + +export const defaultSpec = (): Spec => ({ + records: defaultLogsDefaultColumnsRecords(), +}); + diff --git a/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.status.gen.ts b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/apps/logsdrilldown/plugin/src/generated/logsdrilldowndefaultcolumns/v1alpha1/types.status.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface OperatorState { + // lastEvaluation is the ResourceVersion last evaluated + lastEvaluation: string; + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + state: "success" | "in_progress" | "failed"; + // descriptiveState is an optional more descriptive state field which has no requirements on format + descriptiveState?: string; + // details contains any extra information that is operator-specific + details?: Record; +} + +export const defaultOperatorState = (): OperatorState => ({ + lastEvaluation: "", + state: "success", +}); + +export interface Status { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + operatorStates?: Record; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ +}); + diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index 669c7c46844..287c2ff0bbe 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -10,11 +10,9 @@ replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver require ( github.com/emicklei/go-restful/v3 v3.13.0 - github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 github.com/grafana/grafana-app-sdk v0.48.5 github.com/grafana/grafana-app-sdk/logging v0.48.3 - github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 k8s.io/apiserver v0.34.2 @@ -78,11 +76,13 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect github.com/grafana/grafana-aws-sdk v1.3.0 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect + github.com/grafana/grafana/pkg/apimachinery v0.0.0 // indirect github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect diff --git a/apps/plugins/kinds/manifest.cue b/apps/plugins/kinds/manifest.cue index f9a08562cb0..f624dc117bc 100644 --- a/apps/plugins/kinds/manifest.cue +++ b/apps/plugins/kinds/manifest.cue @@ -16,6 +16,6 @@ v0alpha1Version: { } kinds: [ pluginV0Alpha1, - pluginMetaV0Alpha1, + metaV0Alpha1, ] } diff --git a/apps/plugins/kinds/pluginmeta.cue b/apps/plugins/kinds/meta.cue similarity index 98% rename from apps/plugins/kinds/pluginmeta.cue rename to apps/plugins/kinds/meta.cue index 03b1ff10ff2..34a7b9aaf12 100644 --- a/apps/plugins/kinds/pluginmeta.cue +++ b/apps/plugins/kinds/meta.cue @@ -1,8 +1,7 @@ package plugins -pluginMetaV0Alpha1: { - kind: "PluginMeta" - plural: "pluginsmeta" +metaV0Alpha1: { + kind: "Meta" scope: "Namespaced" schema: { spec: { diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_client_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_client_gen.go similarity index 50% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_client_gen.go rename to apps/plugins/pkg/apis/plugins/v0alpha1/meta_client_gen.go index e7788e27a33..4acc2b635e8 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_client_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_client_gen.go @@ -7,33 +7,33 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type PluginMetaClient struct { - client *resource.TypedClient[*PluginMeta, *PluginMetaList] +type MetaClient struct { + client *resource.TypedClient[*Meta, *MetaList] } -func NewPluginMetaClient(client resource.Client) *PluginMetaClient { - return &PluginMetaClient{ - client: resource.NewTypedClient[*PluginMeta, *PluginMetaList](client, PluginMetaKind()), +func NewMetaClient(client resource.Client) *MetaClient { + return &MetaClient{ + client: resource.NewTypedClient[*Meta, *MetaList](client, MetaKind()), } } -func NewPluginMetaClientFromGenerator(generator resource.ClientGenerator) (*PluginMetaClient, error) { - c, err := generator.ClientFor(PluginMetaKind()) +func NewMetaClientFromGenerator(generator resource.ClientGenerator) (*MetaClient, error) { + c, err := generator.ClientFor(MetaKind()) if err != nil { return nil, err } - return NewPluginMetaClient(c), nil + return NewMetaClient(c), nil } -func (c *PluginMetaClient) Get(ctx context.Context, identifier resource.Identifier) (*PluginMeta, error) { +func (c *MetaClient) Get(ctx context.Context, identifier resource.Identifier) (*Meta, error) { return c.client.Get(ctx, identifier) } -func (c *PluginMetaClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginMetaList, error) { +func (c *MetaClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*MetaList, error) { return c.client.List(ctx, namespace, opts) } -func (c *PluginMetaClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*PluginMetaList, error) { +func (c *MetaClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*MetaList, error) { resp, err := c.client.List(ctx, namespace, resource.ListOptions{ ResourceVersion: opts.ResourceVersion, Limit: opts.Limit, @@ -61,25 +61,25 @@ func (c *PluginMetaClient) ListAll(ctx context.Context, namespace string, opts r return resp, nil } -func (c *PluginMetaClient) Create(ctx context.Context, obj *PluginMeta, opts resource.CreateOptions) (*PluginMeta, error) { +func (c *MetaClient) Create(ctx context.Context, obj *Meta, opts resource.CreateOptions) (*Meta, error) { // Make sure apiVersion and kind are set obj.APIVersion = GroupVersion.Identifier() - obj.Kind = PluginMetaKind().Kind() + obj.Kind = MetaKind().Kind() return c.client.Create(ctx, obj, opts) } -func (c *PluginMetaClient) Update(ctx context.Context, obj *PluginMeta, opts resource.UpdateOptions) (*PluginMeta, error) { +func (c *MetaClient) Update(ctx context.Context, obj *Meta, opts resource.UpdateOptions) (*Meta, error) { return c.client.Update(ctx, obj, opts) } -func (c *PluginMetaClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*PluginMeta, error) { +func (c *MetaClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Meta, error) { return c.client.Patch(ctx, identifier, req, opts) } -func (c *PluginMetaClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus PluginMetaStatus, opts resource.UpdateOptions) (*PluginMeta, error) { - return c.client.Update(ctx, &PluginMeta{ +func (c *MetaClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus MetaStatus, opts resource.UpdateOptions) (*Meta, error) { + return c.client.Update(ctx, &Meta{ TypeMeta: metav1.TypeMeta{ - Kind: PluginMetaKind().Kind(), + Kind: MetaKind().Kind(), APIVersion: GroupVersion.Identifier(), }, ObjectMeta: metav1.ObjectMeta{ @@ -94,6 +94,6 @@ func (c *PluginMetaClient) UpdateStatus(ctx context.Context, identifier resource }) } -func (c *PluginMetaClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { +func (c *MetaClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { return c.client.Delete(ctx, identifier, opts) } diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_codec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_codec_gen.go similarity index 56% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_codec_gen.go rename to apps/plugins/pkg/apis/plugins/v0alpha1/meta_codec_gen.go index 77fb6f918a2..c152eb63e79 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_codec_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_codec_gen.go @@ -11,18 +11,18 @@ import ( "github.com/grafana/grafana-app-sdk/resource" ) -// PluginMetaJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding -type PluginMetaJSONCodec struct{} +// MetaJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type MetaJSONCodec struct{} // Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` -func (*PluginMetaJSONCodec) Read(reader io.Reader, into resource.Object) error { +func (*MetaJSONCodec) Read(reader io.Reader, into resource.Object) error { return json.NewDecoder(reader).Decode(into) } // Write writes JSON-encoded bytes into `writer` marshaled from `from` -func (*PluginMetaJSONCodec) Write(writer io.Writer, from resource.Object) error { +func (*MetaJSONCodec) Write(writer io.Writer, from resource.Object) error { return json.NewEncoder(writer).Encode(from) } // Interface compliance checks -var _ resource.Codec = &PluginMetaJSONCodec{} +var _ resource.Codec = &MetaJSONCodec{} diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_metadata_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_metadata_gen.go similarity index 85% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_metadata_gen.go rename to apps/plugins/pkg/apis/plugins/v0alpha1/meta_metadata_gen.go index 7d3b3c9c6b8..9de0658352f 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_metadata_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_metadata_gen.go @@ -9,7 +9,7 @@ import ( // metadata contains embedded CommonMetadata and can be extended with custom string fields // TODO: use CommonMetadata instead of redefining here; currently needs to be defined here // without external reference as using the CommonMetadata reference breaks thema codegen. -type PluginMetaMetadata struct { +type MetaMetadata struct { UpdateTimestamp time.Time `json:"updateTimestamp"` CreatedBy string `json:"createdBy"` Uid string `json:"uid"` @@ -22,9 +22,9 @@ type PluginMetaMetadata struct { Labels map[string]string `json:"labels"` } -// NewPluginMetaMetadata creates a new PluginMetaMetadata object. -func NewPluginMetaMetadata() *PluginMetaMetadata { - return &PluginMetaMetadata{ +// NewMetaMetadata creates a new MetaMetadata object. +func NewMetaMetadata() *MetaMetadata { + return &MetaMetadata{ Finalizers: []string{}, Labels: map[string]string{}, } diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_object_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_object_gen.go similarity index 68% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_object_gen.go rename to apps/plugins/pkg/apis/plugins/v0alpha1/meta_object_gen.go index dac431ebf12..e42a1ad5d80 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_object_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_object_gen.go @@ -15,22 +15,29 @@ import ( ) // +k8s:openapi-gen=true -type PluginMeta struct { +type Meta struct { metav1.TypeMeta `json:",inline" yaml:",inline"` metav1.ObjectMeta `json:"metadata" yaml:"metadata"` - // Spec is the spec of the PluginMeta - Spec PluginMetaSpec `json:"spec" yaml:"spec"` + // Spec is the spec of the Meta + Spec MetaSpec `json:"spec" yaml:"spec"` - Status PluginMetaStatus `json:"status" yaml:"status"` + Status MetaStatus `json:"status" yaml:"status"` } -func (o *PluginMeta) GetSpec() any { +func NewMeta() *Meta { + return &Meta{ + Spec: *NewMetaSpec(), + Status: *NewMetaStatus(), + } +} + +func (o *Meta) GetSpec() any { return o.Spec } -func (o *PluginMeta) SetSpec(spec any) error { - cast, ok := spec.(PluginMetaSpec) +func (o *Meta) SetSpec(spec any) error { + cast, ok := spec.(MetaSpec) if !ok { return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) } @@ -38,13 +45,13 @@ func (o *PluginMeta) SetSpec(spec any) error { return nil } -func (o *PluginMeta) GetSubresources() map[string]any { +func (o *Meta) GetSubresources() map[string]any { return map[string]any{ "status": o.Status, } } -func (o *PluginMeta) GetSubresource(name string) (any, bool) { +func (o *Meta) GetSubresource(name string) (any, bool) { switch name { case "status": return o.Status, true @@ -53,12 +60,12 @@ func (o *PluginMeta) GetSubresource(name string) (any, bool) { } } -func (o *PluginMeta) SetSubresource(name string, value any) error { +func (o *Meta) SetSubresource(name string, value any) error { switch name { case "status": - cast, ok := value.(PluginMetaStatus) + cast, ok := value.(MetaStatus) if !ok { - return fmt.Errorf("cannot set status type %#v, not of type PluginMetaStatus", value) + return fmt.Errorf("cannot set status type %#v, not of type MetaStatus", value) } o.Status = cast return nil @@ -67,7 +74,7 @@ func (o *PluginMeta) SetSubresource(name string, value any) error { } } -func (o *PluginMeta) GetStaticMetadata() resource.StaticMetadata { +func (o *Meta) GetStaticMetadata() resource.StaticMetadata { gvk := o.GroupVersionKind() return resource.StaticMetadata{ Name: o.ObjectMeta.Name, @@ -78,7 +85,7 @@ func (o *PluginMeta) GetStaticMetadata() resource.StaticMetadata { } } -func (o *PluginMeta) SetStaticMetadata(metadata resource.StaticMetadata) { +func (o *Meta) SetStaticMetadata(metadata resource.StaticMetadata) { o.Name = metadata.Name o.Namespace = metadata.Namespace o.SetGroupVersionKind(schema.GroupVersionKind{ @@ -88,7 +95,7 @@ func (o *PluginMeta) SetStaticMetadata(metadata resource.StaticMetadata) { }) } -func (o *PluginMeta) GetCommonMetadata() resource.CommonMetadata { +func (o *Meta) GetCommonMetadata() resource.CommonMetadata { dt := o.DeletionTimestamp var deletionTimestamp *time.Time if dt != nil { @@ -120,7 +127,7 @@ func (o *PluginMeta) GetCommonMetadata() resource.CommonMetadata { } } -func (o *PluginMeta) SetCommonMetadata(metadata resource.CommonMetadata) { +func (o *Meta) SetCommonMetadata(metadata resource.CommonMetadata) { o.UID = types.UID(metadata.UID) o.ResourceVersion = metadata.ResourceVersion o.Generation = metadata.Generation @@ -165,7 +172,7 @@ func (o *PluginMeta) SetCommonMetadata(metadata resource.CommonMetadata) { } } -func (o *PluginMeta) GetCreatedBy() string { +func (o *Meta) GetCreatedBy() string { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -173,7 +180,7 @@ func (o *PluginMeta) GetCreatedBy() string { return o.ObjectMeta.Annotations["grafana.com/createdBy"] } -func (o *PluginMeta) SetCreatedBy(createdBy string) { +func (o *Meta) SetCreatedBy(createdBy string) { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -181,7 +188,7 @@ func (o *PluginMeta) SetCreatedBy(createdBy string) { o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy } -func (o *PluginMeta) GetUpdateTimestamp() time.Time { +func (o *Meta) GetUpdateTimestamp() time.Time { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -190,7 +197,7 @@ func (o *PluginMeta) GetUpdateTimestamp() time.Time { return parsed } -func (o *PluginMeta) SetUpdateTimestamp(updateTimestamp time.Time) { +func (o *Meta) SetUpdateTimestamp(updateTimestamp time.Time) { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -198,7 +205,7 @@ func (o *PluginMeta) SetUpdateTimestamp(updateTimestamp time.Time) { o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) } -func (o *PluginMeta) GetUpdatedBy() string { +func (o *Meta) GetUpdatedBy() string { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -206,7 +213,7 @@ func (o *PluginMeta) GetUpdatedBy() string { return o.ObjectMeta.Annotations["grafana.com/updatedBy"] } -func (o *PluginMeta) SetUpdatedBy(updatedBy string) { +func (o *Meta) SetUpdatedBy(updatedBy string) { if o.ObjectMeta.Annotations == nil { o.ObjectMeta.Annotations = make(map[string]string) } @@ -214,21 +221,21 @@ func (o *PluginMeta) SetUpdatedBy(updatedBy string) { o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy } -func (o *PluginMeta) Copy() resource.Object { +func (o *Meta) Copy() resource.Object { return resource.CopyObject(o) } -func (o *PluginMeta) DeepCopyObject() runtime.Object { +func (o *Meta) DeepCopyObject() runtime.Object { return o.Copy() } -func (o *PluginMeta) DeepCopy() *PluginMeta { - cpy := &PluginMeta{} +func (o *Meta) DeepCopy() *Meta { + cpy := &Meta{} o.DeepCopyInto(cpy) return cpy } -func (o *PluginMeta) DeepCopyInto(dst *PluginMeta) { +func (o *Meta) DeepCopyInto(dst *Meta) { dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion dst.TypeMeta.Kind = o.TypeMeta.Kind o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) @@ -237,34 +244,34 @@ func (o *PluginMeta) DeepCopyInto(dst *PluginMeta) { } // Interface compliance compile-time check -var _ resource.Object = &PluginMeta{} +var _ resource.Object = &Meta{} // +k8s:openapi-gen=true -type PluginMetaList struct { +type MetaList struct { metav1.TypeMeta `json:",inline" yaml:",inline"` metav1.ListMeta `json:"metadata" yaml:"metadata"` - Items []PluginMeta `json:"items" yaml:"items"` + Items []Meta `json:"items" yaml:"items"` } -func (o *PluginMetaList) DeepCopyObject() runtime.Object { +func (o *MetaList) DeepCopyObject() runtime.Object { return o.Copy() } -func (o *PluginMetaList) Copy() resource.ListObject { - cpy := &PluginMetaList{ +func (o *MetaList) Copy() resource.ListObject { + cpy := &MetaList{ TypeMeta: o.TypeMeta, - Items: make([]PluginMeta, len(o.Items)), + Items: make([]Meta, len(o.Items)), } o.ListMeta.DeepCopyInto(&cpy.ListMeta) for i := 0; i < len(o.Items); i++ { - if item, ok := o.Items[i].Copy().(*PluginMeta); ok { + if item, ok := o.Items[i].Copy().(*Meta); ok { cpy.Items[i] = *item } } return cpy } -func (o *PluginMetaList) GetItems() []resource.Object { +func (o *MetaList) GetItems() []resource.Object { items := make([]resource.Object, len(o.Items)) for i := 0; i < len(o.Items); i++ { items[i] = &o.Items[i] @@ -272,48 +279,48 @@ func (o *PluginMetaList) GetItems() []resource.Object { return items } -func (o *PluginMetaList) SetItems(items []resource.Object) { - o.Items = make([]PluginMeta, len(items)) +func (o *MetaList) SetItems(items []resource.Object) { + o.Items = make([]Meta, len(items)) for i := 0; i < len(items); i++ { - o.Items[i] = *items[i].(*PluginMeta) + o.Items[i] = *items[i].(*Meta) } } -func (o *PluginMetaList) DeepCopy() *PluginMetaList { - cpy := &PluginMetaList{} +func (o *MetaList) DeepCopy() *MetaList { + cpy := &MetaList{} o.DeepCopyInto(cpy) return cpy } -func (o *PluginMetaList) DeepCopyInto(dst *PluginMetaList) { +func (o *MetaList) DeepCopyInto(dst *MetaList) { resource.CopyObjectInto(dst, o) } // Interface compliance compile-time check -var _ resource.ListObject = &PluginMetaList{} +var _ resource.ListObject = &MetaList{} // Copy methods for all subresource types // DeepCopy creates a full deep copy of Spec -func (s *PluginMetaSpec) DeepCopy() *PluginMetaSpec { - cpy := &PluginMetaSpec{} +func (s *MetaSpec) DeepCopy() *MetaSpec { + cpy := &MetaSpec{} s.DeepCopyInto(cpy) return cpy } // DeepCopyInto deep copies Spec into another Spec object -func (s *PluginMetaSpec) DeepCopyInto(dst *PluginMetaSpec) { +func (s *MetaSpec) DeepCopyInto(dst *MetaSpec) { resource.CopyObjectInto(dst, s) } -// DeepCopy creates a full deep copy of PluginMetaStatus -func (s *PluginMetaStatus) DeepCopy() *PluginMetaStatus { - cpy := &PluginMetaStatus{} +// DeepCopy creates a full deep copy of MetaStatus +func (s *MetaStatus) DeepCopy() *MetaStatus { + cpy := &MetaStatus{} s.DeepCopyInto(cpy) return cpy } -// DeepCopyInto deep copies PluginMetaStatus into another PluginMetaStatus object -func (s *PluginMetaStatus) DeepCopyInto(dst *PluginMetaStatus) { +// DeepCopyInto deep copies MetaStatus into another MetaStatus object +func (s *MetaStatus) DeepCopyInto(dst *MetaStatus) { resource.CopyObjectInto(dst, s) } diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/meta_schema_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_schema_gen.go new file mode 100644 index 00000000000..38b45140ff6 --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaMeta = resource.NewSimpleSchema("plugins.grafana.app", "v0alpha1", NewMeta(), &MetaList{}, resource.WithKind("Meta"), + resource.WithPlural("metas"), resource.WithScope(resource.NamespacedScope)) + kindMeta = resource.Kind{ + Schema: schemaMeta, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &MetaJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func MetaKind() resource.Kind { + return kindMeta +} + +// Schema returns a resource.SimpleSchema representation of Meta +func MetaSchema() *resource.SimpleSchema { + return schemaMeta +} + +// Interface compliance checks +var _ resource.Schema = kindMeta diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go new file mode 100644 index 00000000000..9598da22ef7 --- /dev/null +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go @@ -0,0 +1,474 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// JSON configuration schema for Grafana plugins +// Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json +// +k8s:openapi-gen=true +type MetaJSONData struct { + // Unique name of the plugin + Id string `json:"id"` + // Plugin type + Type MetaJSONDataType `json:"type"` + // Human-readable name of the plugin + Name string `json:"name"` + // Metadata for the plugin + Info MetaInfo `json:"info"` + // Dependency information + Dependencies MetaDependencies `json:"dependencies"` + // Optional fields + Alerting *bool `json:"alerting,omitempty"` + Annotations *bool `json:"annotations,omitempty"` + AutoEnabled *bool `json:"autoEnabled,omitempty"` + Backend *bool `json:"backend,omitempty"` + BuildMode *string `json:"buildMode,omitempty"` + BuiltIn *bool `json:"builtIn,omitempty"` + Category *MetaJSONDataCategory `json:"category,omitempty"` + EnterpriseFeatures *MetaEnterpriseFeatures `json:"enterpriseFeatures,omitempty"` + Executable *string `json:"executable,omitempty"` + HideFromList *bool `json:"hideFromList,omitempty"` + // +listType=atomic + Includes []MetaInclude `json:"includes,omitempty"` + Logs *bool `json:"logs,omitempty"` + Metrics *bool `json:"metrics,omitempty"` + MultiValueFilterOperators *bool `json:"multiValueFilterOperators,omitempty"` + PascalName *string `json:"pascalName,omitempty"` + Preload *bool `json:"preload,omitempty"` + QueryOptions *MetaQueryOptions `json:"queryOptions,omitempty"` + // +listType=atomic + Routes []MetaRoute `json:"routes,omitempty"` + SkipDataQuery *bool `json:"skipDataQuery,omitempty"` + State *MetaJSONDataState `json:"state,omitempty"` + Streaming *bool `json:"streaming,omitempty"` + Suggestions *bool `json:"suggestions,omitempty"` + Tracing *bool `json:"tracing,omitempty"` + Iam *MetaIAM `json:"iam,omitempty"` + // +listType=atomic + Roles []MetaRole `json:"roles,omitempty"` + Extensions *MetaExtensions `json:"extensions,omitempty"` +} + +// NewMetaJSONData creates a new MetaJSONData object. +func NewMetaJSONData() *MetaJSONData { + return &MetaJSONData{ + Info: *NewMetaInfo(), + Dependencies: *NewMetaDependencies(), + } +} + +// +k8s:openapi-gen=true +type MetaInfo struct { + // Required fields + // +listType=set + Keywords []string `json:"keywords"` + Logos MetaV0alpha1InfoLogos `json:"logos"` + Updated string `json:"updated"` + Version string `json:"version"` + // Optional fields + Author *MetaV0alpha1InfoAuthor `json:"author,omitempty"` + Description *string `json:"description,omitempty"` + // +listType=atomic + Links []MetaV0alpha1InfoLinks `json:"links,omitempty"` + // +listType=atomic + Screenshots []MetaV0alpha1InfoScreenshots `json:"screenshots,omitempty"` +} + +// NewMetaInfo creates a new MetaInfo object. +func NewMetaInfo() *MetaInfo { + return &MetaInfo{ + Keywords: []string{}, + Logos: *NewMetaV0alpha1InfoLogos(), + } +} + +// +k8s:openapi-gen=true +type MetaDependencies struct { + // Required field + GrafanaDependency string `json:"grafanaDependency"` + // Optional fields + GrafanaVersion *string `json:"grafanaVersion,omitempty"` + // +listType=set + // +listMapKey=id + Plugins []MetaV0alpha1DependenciesPlugins `json:"plugins,omitempty"` + Extensions *MetaV0alpha1DependenciesExtensions `json:"extensions,omitempty"` +} + +// NewMetaDependencies creates a new MetaDependencies object. +func NewMetaDependencies() *MetaDependencies { + return &MetaDependencies{} +} + +// +k8s:openapi-gen=true +type MetaEnterpriseFeatures struct { + // Allow additional properties + HealthDiagnosticsErrors *bool `json:"healthDiagnosticsErrors,omitempty"` +} + +// NewMetaEnterpriseFeatures creates a new MetaEnterpriseFeatures object. +func NewMetaEnterpriseFeatures() *MetaEnterpriseFeatures { + return &MetaEnterpriseFeatures{ + HealthDiagnosticsErrors: (func(input bool) *bool { return &input })(false), + } +} + +// +k8s:openapi-gen=true +type MetaInclude struct { + Uid *string `json:"uid,omitempty"` + Type *MetaIncludeType `json:"type,omitempty"` + Name *string `json:"name,omitempty"` + Component *string `json:"component,omitempty"` + Role *MetaIncludeRole `json:"role,omitempty"` + Action *string `json:"action,omitempty"` + Path *string `json:"path,omitempty"` + AddToNav *bool `json:"addToNav,omitempty"` + DefaultNav *bool `json:"defaultNav,omitempty"` + Icon *string `json:"icon,omitempty"` +} + +// NewMetaInclude creates a new MetaInclude object. +func NewMetaInclude() *MetaInclude { + return &MetaInclude{} +} + +// +k8s:openapi-gen=true +type MetaQueryOptions struct { + MaxDataPoints *bool `json:"maxDataPoints,omitempty"` + MinInterval *bool `json:"minInterval,omitempty"` + CacheTimeout *bool `json:"cacheTimeout,omitempty"` +} + +// NewMetaQueryOptions creates a new MetaQueryOptions object. +func NewMetaQueryOptions() *MetaQueryOptions { + return &MetaQueryOptions{} +} + +// +k8s:openapi-gen=true +type MetaRoute struct { + Path *string `json:"path,omitempty"` + Method *string `json:"method,omitempty"` + Url *string `json:"url,omitempty"` + ReqSignedIn *bool `json:"reqSignedIn,omitempty"` + ReqRole *string `json:"reqRole,omitempty"` + ReqAction *string `json:"reqAction,omitempty"` + // +listType=atomic + Headers []string `json:"headers,omitempty"` + Body map[string]interface{} `json:"body,omitempty"` + TokenAuth *MetaV0alpha1RouteTokenAuth `json:"tokenAuth,omitempty"` + JwtTokenAuth *MetaV0alpha1RouteJwtTokenAuth `json:"jwtTokenAuth,omitempty"` + // +listType=atomic + UrlParams []MetaV0alpha1RouteUrlParams `json:"urlParams,omitempty"` +} + +// NewMetaRoute creates a new MetaRoute object. +func NewMetaRoute() *MetaRoute { + return &MetaRoute{} +} + +// +k8s:openapi-gen=true +type MetaIAM struct { + // +listType=atomic + Permissions []MetaV0alpha1IAMPermissions `json:"permissions,omitempty"` +} + +// NewMetaIAM creates a new MetaIAM object. +func NewMetaIAM() *MetaIAM { + return &MetaIAM{} +} + +// +k8s:openapi-gen=true +type MetaRole struct { + Role *MetaV0alpha1RoleRole `json:"role,omitempty"` + // +listType=set + Grants []string `json:"grants,omitempty"` +} + +// NewMetaRole creates a new MetaRole object. +func NewMetaRole() *MetaRole { + return &MetaRole{} +} + +// +k8s:openapi-gen=true +type MetaExtensions struct { + // +listType=atomic + AddedComponents []MetaV0alpha1ExtensionsAddedComponents `json:"addedComponents,omitempty"` + // +listType=atomic + AddedLinks []MetaV0alpha1ExtensionsAddedLinks `json:"addedLinks,omitempty"` + // +listType=set + // +listMapKey=id + ExposedComponents []MetaV0alpha1ExtensionsExposedComponents `json:"exposedComponents,omitempty"` + // +listType=set + // +listMapKey=id + ExtensionPoints []MetaV0alpha1ExtensionsExtensionPoints `json:"extensionPoints,omitempty"` +} + +// NewMetaExtensions creates a new MetaExtensions object. +func NewMetaExtensions() *MetaExtensions { + return &MetaExtensions{} +} + +// +k8s:openapi-gen=true +type MetaSpec struct { + PluginJSON MetaJSONData `json:"pluginJSON"` +} + +// NewMetaSpec creates a new MetaSpec object. +func NewMetaSpec() *MetaSpec { + return &MetaSpec{ + PluginJSON: *NewMetaJSONData(), + } +} + +// +k8s:openapi-gen=true +type MetaV0alpha1InfoLogos struct { + Small string `json:"small"` + Large string `json:"large"` +} + +// NewMetaV0alpha1InfoLogos creates a new MetaV0alpha1InfoLogos object. +func NewMetaV0alpha1InfoLogos() *MetaV0alpha1InfoLogos { + return &MetaV0alpha1InfoLogos{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1InfoAuthor struct { + Name *string `json:"name,omitempty"` + Email *string `json:"email,omitempty"` + Url *string `json:"url,omitempty"` +} + +// NewMetaV0alpha1InfoAuthor creates a new MetaV0alpha1InfoAuthor object. +func NewMetaV0alpha1InfoAuthor() *MetaV0alpha1InfoAuthor { + return &MetaV0alpha1InfoAuthor{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1InfoLinks struct { + Name *string `json:"name,omitempty"` + Url *string `json:"url,omitempty"` +} + +// NewMetaV0alpha1InfoLinks creates a new MetaV0alpha1InfoLinks object. +func NewMetaV0alpha1InfoLinks() *MetaV0alpha1InfoLinks { + return &MetaV0alpha1InfoLinks{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1InfoScreenshots struct { + Name *string `json:"name,omitempty"` + Path *string `json:"path,omitempty"` +} + +// NewMetaV0alpha1InfoScreenshots creates a new MetaV0alpha1InfoScreenshots object. +func NewMetaV0alpha1InfoScreenshots() *MetaV0alpha1InfoScreenshots { + return &MetaV0alpha1InfoScreenshots{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1DependenciesPlugins struct { + Id string `json:"id"` + Type MetaV0alpha1DependenciesPluginsType `json:"type"` + Name string `json:"name"` +} + +// NewMetaV0alpha1DependenciesPlugins creates a new MetaV0alpha1DependenciesPlugins object. +func NewMetaV0alpha1DependenciesPlugins() *MetaV0alpha1DependenciesPlugins { + return &MetaV0alpha1DependenciesPlugins{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1DependenciesExtensions struct { + // +listType=set + ExposedComponents []string `json:"exposedComponents,omitempty"` +} + +// NewMetaV0alpha1DependenciesExtensions creates a new MetaV0alpha1DependenciesExtensions object. +func NewMetaV0alpha1DependenciesExtensions() *MetaV0alpha1DependenciesExtensions { + return &MetaV0alpha1DependenciesExtensions{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1RouteTokenAuth struct { + Url *string `json:"url,omitempty"` + // +listType=set + Scopes []string `json:"scopes,omitempty"` + Params map[string]interface{} `json:"params,omitempty"` +} + +// NewMetaV0alpha1RouteTokenAuth creates a new MetaV0alpha1RouteTokenAuth object. +func NewMetaV0alpha1RouteTokenAuth() *MetaV0alpha1RouteTokenAuth { + return &MetaV0alpha1RouteTokenAuth{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1RouteJwtTokenAuth struct { + Url *string `json:"url,omitempty"` + // +listType=set + Scopes []string `json:"scopes,omitempty"` + Params map[string]interface{} `json:"params,omitempty"` +} + +// NewMetaV0alpha1RouteJwtTokenAuth creates a new MetaV0alpha1RouteJwtTokenAuth object. +func NewMetaV0alpha1RouteJwtTokenAuth() *MetaV0alpha1RouteJwtTokenAuth { + return &MetaV0alpha1RouteJwtTokenAuth{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1RouteUrlParams struct { + Name *string `json:"name,omitempty"` + Content *string `json:"content,omitempty"` +} + +// NewMetaV0alpha1RouteUrlParams creates a new MetaV0alpha1RouteUrlParams object. +func NewMetaV0alpha1RouteUrlParams() *MetaV0alpha1RouteUrlParams { + return &MetaV0alpha1RouteUrlParams{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1IAMPermissions struct { + Action *string `json:"action,omitempty"` + Scope *string `json:"scope,omitempty"` +} + +// NewMetaV0alpha1IAMPermissions creates a new MetaV0alpha1IAMPermissions object. +func NewMetaV0alpha1IAMPermissions() *MetaV0alpha1IAMPermissions { + return &MetaV0alpha1IAMPermissions{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1RoleRolePermissions struct { + Action *string `json:"action,omitempty"` + Scope *string `json:"scope,omitempty"` +} + +// NewMetaV0alpha1RoleRolePermissions creates a new MetaV0alpha1RoleRolePermissions object. +func NewMetaV0alpha1RoleRolePermissions() *MetaV0alpha1RoleRolePermissions { + return &MetaV0alpha1RoleRolePermissions{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1RoleRole struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + // +listType=atomic + Permissions []MetaV0alpha1RoleRolePermissions `json:"permissions,omitempty"` +} + +// NewMetaV0alpha1RoleRole creates a new MetaV0alpha1RoleRole object. +func NewMetaV0alpha1RoleRole() *MetaV0alpha1RoleRole { + return &MetaV0alpha1RoleRole{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1ExtensionsAddedComponents struct { + // +listType=set + Targets []string `json:"targets"` + Title string `json:"title"` + Description *string `json:"description,omitempty"` +} + +// NewMetaV0alpha1ExtensionsAddedComponents creates a new MetaV0alpha1ExtensionsAddedComponents object. +func NewMetaV0alpha1ExtensionsAddedComponents() *MetaV0alpha1ExtensionsAddedComponents { + return &MetaV0alpha1ExtensionsAddedComponents{ + Targets: []string{}, + } +} + +// +k8s:openapi-gen=true +type MetaV0alpha1ExtensionsAddedLinks struct { + // +listType=set + Targets []string `json:"targets"` + Title string `json:"title"` + Description *string `json:"description,omitempty"` +} + +// NewMetaV0alpha1ExtensionsAddedLinks creates a new MetaV0alpha1ExtensionsAddedLinks object. +func NewMetaV0alpha1ExtensionsAddedLinks() *MetaV0alpha1ExtensionsAddedLinks { + return &MetaV0alpha1ExtensionsAddedLinks{ + Targets: []string{}, + } +} + +// +k8s:openapi-gen=true +type MetaV0alpha1ExtensionsExposedComponents struct { + Id string `json:"id"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` +} + +// NewMetaV0alpha1ExtensionsExposedComponents creates a new MetaV0alpha1ExtensionsExposedComponents object. +func NewMetaV0alpha1ExtensionsExposedComponents() *MetaV0alpha1ExtensionsExposedComponents { + return &MetaV0alpha1ExtensionsExposedComponents{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1ExtensionsExtensionPoints struct { + Id string `json:"id"` + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` +} + +// NewMetaV0alpha1ExtensionsExtensionPoints creates a new MetaV0alpha1ExtensionsExtensionPoints object. +func NewMetaV0alpha1ExtensionsExtensionPoints() *MetaV0alpha1ExtensionsExtensionPoints { + return &MetaV0alpha1ExtensionsExtensionPoints{} +} + +// +k8s:openapi-gen=true +type MetaJSONDataType string + +const ( + MetaJSONDataTypeApp MetaJSONDataType = "app" + MetaJSONDataTypeDatasource MetaJSONDataType = "datasource" + MetaJSONDataTypePanel MetaJSONDataType = "panel" + MetaJSONDataTypeRenderer MetaJSONDataType = "renderer" +) + +// +k8s:openapi-gen=true +type MetaJSONDataCategory string + +const ( + MetaJSONDataCategoryTsdb MetaJSONDataCategory = "tsdb" + MetaJSONDataCategoryLogging MetaJSONDataCategory = "logging" + MetaJSONDataCategoryCloud MetaJSONDataCategory = "cloud" + MetaJSONDataCategoryTracing MetaJSONDataCategory = "tracing" + MetaJSONDataCategoryProfiling MetaJSONDataCategory = "profiling" + MetaJSONDataCategorySql MetaJSONDataCategory = "sql" + MetaJSONDataCategoryEnterprise MetaJSONDataCategory = "enterprise" + MetaJSONDataCategoryIot MetaJSONDataCategory = "iot" + MetaJSONDataCategoryOther MetaJSONDataCategory = "other" +) + +// +k8s:openapi-gen=true +type MetaJSONDataState string + +const ( + MetaJSONDataStateAlpha MetaJSONDataState = "alpha" + MetaJSONDataStateBeta MetaJSONDataState = "beta" +) + +// +k8s:openapi-gen=true +type MetaIncludeType string + +const ( + MetaIncludeTypeDashboard MetaIncludeType = "dashboard" + MetaIncludeTypePage MetaIncludeType = "page" + MetaIncludeTypePanel MetaIncludeType = "panel" + MetaIncludeTypeDatasource MetaIncludeType = "datasource" +) + +// +k8s:openapi-gen=true +type MetaIncludeRole string + +const ( + MetaIncludeRoleAdmin MetaIncludeRole = "Admin" + MetaIncludeRoleEditor MetaIncludeRole = "Editor" + MetaIncludeRoleViewer MetaIncludeRole = "Viewer" +) + +// +k8s:openapi-gen=true +type MetaV0alpha1DependenciesPluginsType string + +const ( + MetaV0alpha1DependenciesPluginsTypeApp MetaV0alpha1DependenciesPluginsType = "app" + MetaV0alpha1DependenciesPluginsTypeDatasource MetaV0alpha1DependenciesPluginsType = "datasource" + MetaV0alpha1DependenciesPluginsTypePanel MetaV0alpha1DependenciesPluginsType = "panel" +) diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_status_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_status_gen.go similarity index 52% rename from apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_status_gen.go rename to apps/plugins/pkg/apis/plugins/v0alpha1/meta_status_gen.go index 60fa37dbb32..5f37ac58fb7 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_status_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_status_gen.go @@ -3,42 +3,42 @@ package v0alpha1 // +k8s:openapi-gen=true -type PluginMetastatusOperatorState struct { +type MetastatusOperatorState 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 PluginMetaStatusOperatorStateState `json:"state"` + State MetaStatusOperatorStateState `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"` } -// NewPluginMetastatusOperatorState creates a new PluginMetastatusOperatorState object. -func NewPluginMetastatusOperatorState() *PluginMetastatusOperatorState { - return &PluginMetastatusOperatorState{} +// NewMetastatusOperatorState creates a new MetastatusOperatorState object. +func NewMetastatusOperatorState() *MetastatusOperatorState { + return &MetastatusOperatorState{} } // +k8s:openapi-gen=true -type PluginMetaStatus struct { +type MetaStatus 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]PluginMetastatusOperatorState `json:"operatorStates,omitempty"` + OperatorStates map[string]MetastatusOperatorState `json:"operatorStates,omitempty"` // additionalFields is reserved for future use AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` } -// NewPluginMetaStatus creates a new PluginMetaStatus object. -func NewPluginMetaStatus() *PluginMetaStatus { - return &PluginMetaStatus{} +// NewMetaStatus creates a new MetaStatus object. +func NewMetaStatus() *MetaStatus { + return &MetaStatus{} } // +k8s:openapi-gen=true -type PluginMetaStatusOperatorStateState string +type MetaStatusOperatorStateState string const ( - PluginMetaStatusOperatorStateStateSuccess PluginMetaStatusOperatorStateState = "success" - PluginMetaStatusOperatorStateStateInProgress PluginMetaStatusOperatorStateState = "in_progress" - PluginMetaStatusOperatorStateStateFailed PluginMetaStatusOperatorStateState = "failed" + MetaStatusOperatorStateStateSuccess MetaStatusOperatorStateState = "success" + MetaStatusOperatorStateStateInProgress MetaStatusOperatorStateState = "in_progress" + MetaStatusOperatorStateStateFailed MetaStatusOperatorStateState = "failed" ) diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_object_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_object_gen.go index d2cdedba399..b92b11f4cba 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_object_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_object_gen.go @@ -25,6 +25,13 @@ type Plugin struct { Status PluginStatus `json:"status" yaml:"status"` } +func NewPlugin() *Plugin { + return &Plugin{ + Spec: *NewPluginSpec(), + Status: *NewPluginStatus(), + } +} + func (o *Plugin) GetSpec() any { return o.Spec } diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_schema_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_schema_gen.go index 97024275a1c..144ddd5af89 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_schema_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaPlugin = resource.NewSimpleSchema("plugins.grafana.app", "v0alpha1", &Plugin{}, &PluginList{}, resource.WithKind("Plugin"), + schemaPlugin = resource.NewSimpleSchema("plugins.grafana.app", "v0alpha1", NewPlugin(), &PluginList{}, resource.WithKind("Plugin"), resource.WithPlural("plugins"), resource.WithScope(resource.NamespacedScope)) kindPlugin = resource.Kind{ Schema: schemaPlugin, diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_schema_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_schema_gen.go deleted file mode 100644 index a4022c8de97..00000000000 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_schema_gen.go +++ /dev/null @@ -1,34 +0,0 @@ -// -// Code generated by grafana-app-sdk. DO NOT EDIT. -// - -package v0alpha1 - -import ( - "github.com/grafana/grafana-app-sdk/resource" -) - -// schema is unexported to prevent accidental overwrites -var ( - schemaPluginMeta = resource.NewSimpleSchema("plugins.grafana.app", "v0alpha1", &PluginMeta{}, &PluginMetaList{}, resource.WithKind("PluginMeta"), - resource.WithPlural("pluginmetas"), resource.WithScope(resource.NamespacedScope)) - kindPluginMeta = resource.Kind{ - Schema: schemaPluginMeta, - Codecs: map[resource.KindEncoding]resource.Codec{ - resource.KindEncodingJSON: &PluginMetaJSONCodec{}, - }, - } -) - -// Kind returns a resource.Kind for this Schema with a JSON codec -func PluginMetaKind() resource.Kind { - return kindPluginMeta -} - -// Schema returns a resource.SimpleSchema representation of PluginMeta -func PluginMetaSchema() *resource.SimpleSchema { - return schemaPluginMeta -} - -// Interface compliance checks -var _ resource.Schema = kindPluginMeta diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_spec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_spec_gen.go deleted file mode 100644 index 14a3b0515bd..00000000000 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/pluginmeta_spec_gen.go +++ /dev/null @@ -1,474 +0,0 @@ -// Code generated - EDITING IS FUTILE. DO NOT EDIT. - -package v0alpha1 - -// JSON configuration schema for Grafana plugins -// Converted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json -// +k8s:openapi-gen=true -type PluginMetaJSONData struct { - // Unique name of the plugin - Id string `json:"id"` - // Plugin type - Type PluginMetaJSONDataType `json:"type"` - // Human-readable name of the plugin - Name string `json:"name"` - // Metadata for the plugin - Info PluginMetaInfo `json:"info"` - // Dependency information - Dependencies PluginMetaDependencies `json:"dependencies"` - // Optional fields - Alerting *bool `json:"alerting,omitempty"` - Annotations *bool `json:"annotations,omitempty"` - AutoEnabled *bool `json:"autoEnabled,omitempty"` - Backend *bool `json:"backend,omitempty"` - BuildMode *string `json:"buildMode,omitempty"` - BuiltIn *bool `json:"builtIn,omitempty"` - Category *PluginMetaJSONDataCategory `json:"category,omitempty"` - EnterpriseFeatures *PluginMetaEnterpriseFeatures `json:"enterpriseFeatures,omitempty"` - Executable *string `json:"executable,omitempty"` - HideFromList *bool `json:"hideFromList,omitempty"` - // +listType=atomic - Includes []PluginMetaInclude `json:"includes,omitempty"` - Logs *bool `json:"logs,omitempty"` - Metrics *bool `json:"metrics,omitempty"` - MultiValueFilterOperators *bool `json:"multiValueFilterOperators,omitempty"` - PascalName *string `json:"pascalName,omitempty"` - Preload *bool `json:"preload,omitempty"` - QueryOptions *PluginMetaQueryOptions `json:"queryOptions,omitempty"` - // +listType=atomic - Routes []PluginMetaRoute `json:"routes,omitempty"` - SkipDataQuery *bool `json:"skipDataQuery,omitempty"` - State *PluginMetaJSONDataState `json:"state,omitempty"` - Streaming *bool `json:"streaming,omitempty"` - Suggestions *bool `json:"suggestions,omitempty"` - Tracing *bool `json:"tracing,omitempty"` - Iam *PluginMetaIAM `json:"iam,omitempty"` - // +listType=atomic - Roles []PluginMetaRole `json:"roles,omitempty"` - Extensions *PluginMetaExtensions `json:"extensions,omitempty"` -} - -// NewPluginMetaJSONData creates a new PluginMetaJSONData object. -func NewPluginMetaJSONData() *PluginMetaJSONData { - return &PluginMetaJSONData{ - Info: *NewPluginMetaInfo(), - Dependencies: *NewPluginMetaDependencies(), - } -} - -// +k8s:openapi-gen=true -type PluginMetaInfo struct { - // Required fields - // +listType=set - Keywords []string `json:"keywords"` - Logos PluginMetaV0alpha1InfoLogos `json:"logos"` - Updated string `json:"updated"` - Version string `json:"version"` - // Optional fields - Author *PluginMetaV0alpha1InfoAuthor `json:"author,omitempty"` - Description *string `json:"description,omitempty"` - // +listType=atomic - Links []PluginMetaV0alpha1InfoLinks `json:"links,omitempty"` - // +listType=atomic - Screenshots []PluginMetaV0alpha1InfoScreenshots `json:"screenshots,omitempty"` -} - -// NewPluginMetaInfo creates a new PluginMetaInfo object. -func NewPluginMetaInfo() *PluginMetaInfo { - return &PluginMetaInfo{ - Keywords: []string{}, - Logos: *NewPluginMetaV0alpha1InfoLogos(), - } -} - -// +k8s:openapi-gen=true -type PluginMetaDependencies struct { - // Required field - GrafanaDependency string `json:"grafanaDependency"` - // Optional fields - GrafanaVersion *string `json:"grafanaVersion,omitempty"` - // +listType=set - // +listMapKey=id - Plugins []PluginMetaV0alpha1DependenciesPlugins `json:"plugins,omitempty"` - Extensions *PluginMetaV0alpha1DependenciesExtensions `json:"extensions,omitempty"` -} - -// NewPluginMetaDependencies creates a new PluginMetaDependencies object. -func NewPluginMetaDependencies() *PluginMetaDependencies { - return &PluginMetaDependencies{} -} - -// +k8s:openapi-gen=true -type PluginMetaEnterpriseFeatures struct { - // Allow additional properties - HealthDiagnosticsErrors *bool `json:"healthDiagnosticsErrors,omitempty"` -} - -// NewPluginMetaEnterpriseFeatures creates a new PluginMetaEnterpriseFeatures object. -func NewPluginMetaEnterpriseFeatures() *PluginMetaEnterpriseFeatures { - return &PluginMetaEnterpriseFeatures{ - HealthDiagnosticsErrors: (func(input bool) *bool { return &input })(false), - } -} - -// +k8s:openapi-gen=true -type PluginMetaInclude struct { - Uid *string `json:"uid,omitempty"` - Type *PluginMetaIncludeType `json:"type,omitempty"` - Name *string `json:"name,omitempty"` - Component *string `json:"component,omitempty"` - Role *PluginMetaIncludeRole `json:"role,omitempty"` - Action *string `json:"action,omitempty"` - Path *string `json:"path,omitempty"` - AddToNav *bool `json:"addToNav,omitempty"` - DefaultNav *bool `json:"defaultNav,omitempty"` - Icon *string `json:"icon,omitempty"` -} - -// NewPluginMetaInclude creates a new PluginMetaInclude object. -func NewPluginMetaInclude() *PluginMetaInclude { - return &PluginMetaInclude{} -} - -// +k8s:openapi-gen=true -type PluginMetaQueryOptions struct { - MaxDataPoints *bool `json:"maxDataPoints,omitempty"` - MinInterval *bool `json:"minInterval,omitempty"` - CacheTimeout *bool `json:"cacheTimeout,omitempty"` -} - -// NewPluginMetaQueryOptions creates a new PluginMetaQueryOptions object. -func NewPluginMetaQueryOptions() *PluginMetaQueryOptions { - return &PluginMetaQueryOptions{} -} - -// +k8s:openapi-gen=true -type PluginMetaRoute struct { - Path *string `json:"path,omitempty"` - Method *string `json:"method,omitempty"` - Url *string `json:"url,omitempty"` - ReqSignedIn *bool `json:"reqSignedIn,omitempty"` - ReqRole *string `json:"reqRole,omitempty"` - ReqAction *string `json:"reqAction,omitempty"` - // +listType=atomic - Headers []string `json:"headers,omitempty"` - Body map[string]interface{} `json:"body,omitempty"` - TokenAuth *PluginMetaV0alpha1RouteTokenAuth `json:"tokenAuth,omitempty"` - JwtTokenAuth *PluginMetaV0alpha1RouteJwtTokenAuth `json:"jwtTokenAuth,omitempty"` - // +listType=atomic - UrlParams []PluginMetaV0alpha1RouteUrlParams `json:"urlParams,omitempty"` -} - -// NewPluginMetaRoute creates a new PluginMetaRoute object. -func NewPluginMetaRoute() *PluginMetaRoute { - return &PluginMetaRoute{} -} - -// +k8s:openapi-gen=true -type PluginMetaIAM struct { - // +listType=atomic - Permissions []PluginMetaV0alpha1IAMPermissions `json:"permissions,omitempty"` -} - -// NewPluginMetaIAM creates a new PluginMetaIAM object. -func NewPluginMetaIAM() *PluginMetaIAM { - return &PluginMetaIAM{} -} - -// +k8s:openapi-gen=true -type PluginMetaRole struct { - Role *PluginMetaV0alpha1RoleRole `json:"role,omitempty"` - // +listType=set - Grants []string `json:"grants,omitempty"` -} - -// NewPluginMetaRole creates a new PluginMetaRole object. -func NewPluginMetaRole() *PluginMetaRole { - return &PluginMetaRole{} -} - -// +k8s:openapi-gen=true -type PluginMetaExtensions struct { - // +listType=atomic - AddedComponents []PluginMetaV0alpha1ExtensionsAddedComponents `json:"addedComponents,omitempty"` - // +listType=atomic - AddedLinks []PluginMetaV0alpha1ExtensionsAddedLinks `json:"addedLinks,omitempty"` - // +listType=set - // +listMapKey=id - ExposedComponents []PluginMetaV0alpha1ExtensionsExposedComponents `json:"exposedComponents,omitempty"` - // +listType=set - // +listMapKey=id - ExtensionPoints []PluginMetaV0alpha1ExtensionsExtensionPoints `json:"extensionPoints,omitempty"` -} - -// NewPluginMetaExtensions creates a new PluginMetaExtensions object. -func NewPluginMetaExtensions() *PluginMetaExtensions { - return &PluginMetaExtensions{} -} - -// +k8s:openapi-gen=true -type PluginMetaSpec struct { - PluginJSON PluginMetaJSONData `json:"pluginJSON"` -} - -// NewPluginMetaSpec creates a new PluginMetaSpec object. -func NewPluginMetaSpec() *PluginMetaSpec { - return &PluginMetaSpec{ - PluginJSON: *NewPluginMetaJSONData(), - } -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1InfoLogos struct { - Small string `json:"small"` - Large string `json:"large"` -} - -// NewPluginMetaV0alpha1InfoLogos creates a new PluginMetaV0alpha1InfoLogos object. -func NewPluginMetaV0alpha1InfoLogos() *PluginMetaV0alpha1InfoLogos { - return &PluginMetaV0alpha1InfoLogos{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1InfoAuthor struct { - Name *string `json:"name,omitempty"` - Email *string `json:"email,omitempty"` - Url *string `json:"url,omitempty"` -} - -// NewPluginMetaV0alpha1InfoAuthor creates a new PluginMetaV0alpha1InfoAuthor object. -func NewPluginMetaV0alpha1InfoAuthor() *PluginMetaV0alpha1InfoAuthor { - return &PluginMetaV0alpha1InfoAuthor{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1InfoLinks struct { - Name *string `json:"name,omitempty"` - Url *string `json:"url,omitempty"` -} - -// NewPluginMetaV0alpha1InfoLinks creates a new PluginMetaV0alpha1InfoLinks object. -func NewPluginMetaV0alpha1InfoLinks() *PluginMetaV0alpha1InfoLinks { - return &PluginMetaV0alpha1InfoLinks{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1InfoScreenshots struct { - Name *string `json:"name,omitempty"` - Path *string `json:"path,omitempty"` -} - -// NewPluginMetaV0alpha1InfoScreenshots creates a new PluginMetaV0alpha1InfoScreenshots object. -func NewPluginMetaV0alpha1InfoScreenshots() *PluginMetaV0alpha1InfoScreenshots { - return &PluginMetaV0alpha1InfoScreenshots{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1DependenciesPlugins struct { - Id string `json:"id"` - Type PluginMetaV0alpha1DependenciesPluginsType `json:"type"` - Name string `json:"name"` -} - -// NewPluginMetaV0alpha1DependenciesPlugins creates a new PluginMetaV0alpha1DependenciesPlugins object. -func NewPluginMetaV0alpha1DependenciesPlugins() *PluginMetaV0alpha1DependenciesPlugins { - return &PluginMetaV0alpha1DependenciesPlugins{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1DependenciesExtensions struct { - // +listType=set - ExposedComponents []string `json:"exposedComponents,omitempty"` -} - -// NewPluginMetaV0alpha1DependenciesExtensions creates a new PluginMetaV0alpha1DependenciesExtensions object. -func NewPluginMetaV0alpha1DependenciesExtensions() *PluginMetaV0alpha1DependenciesExtensions { - return &PluginMetaV0alpha1DependenciesExtensions{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1RouteTokenAuth struct { - Url *string `json:"url,omitempty"` - // +listType=set - Scopes []string `json:"scopes,omitempty"` - Params map[string]interface{} `json:"params,omitempty"` -} - -// NewPluginMetaV0alpha1RouteTokenAuth creates a new PluginMetaV0alpha1RouteTokenAuth object. -func NewPluginMetaV0alpha1RouteTokenAuth() *PluginMetaV0alpha1RouteTokenAuth { - return &PluginMetaV0alpha1RouteTokenAuth{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1RouteJwtTokenAuth struct { - Url *string `json:"url,omitempty"` - // +listType=set - Scopes []string `json:"scopes,omitempty"` - Params map[string]interface{} `json:"params,omitempty"` -} - -// NewPluginMetaV0alpha1RouteJwtTokenAuth creates a new PluginMetaV0alpha1RouteJwtTokenAuth object. -func NewPluginMetaV0alpha1RouteJwtTokenAuth() *PluginMetaV0alpha1RouteJwtTokenAuth { - return &PluginMetaV0alpha1RouteJwtTokenAuth{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1RouteUrlParams struct { - Name *string `json:"name,omitempty"` - Content *string `json:"content,omitempty"` -} - -// NewPluginMetaV0alpha1RouteUrlParams creates a new PluginMetaV0alpha1RouteUrlParams object. -func NewPluginMetaV0alpha1RouteUrlParams() *PluginMetaV0alpha1RouteUrlParams { - return &PluginMetaV0alpha1RouteUrlParams{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1IAMPermissions struct { - Action *string `json:"action,omitempty"` - Scope *string `json:"scope,omitempty"` -} - -// NewPluginMetaV0alpha1IAMPermissions creates a new PluginMetaV0alpha1IAMPermissions object. -func NewPluginMetaV0alpha1IAMPermissions() *PluginMetaV0alpha1IAMPermissions { - return &PluginMetaV0alpha1IAMPermissions{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1RoleRolePermissions struct { - Action *string `json:"action,omitempty"` - Scope *string `json:"scope,omitempty"` -} - -// NewPluginMetaV0alpha1RoleRolePermissions creates a new PluginMetaV0alpha1RoleRolePermissions object. -func NewPluginMetaV0alpha1RoleRolePermissions() *PluginMetaV0alpha1RoleRolePermissions { - return &PluginMetaV0alpha1RoleRolePermissions{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1RoleRole struct { - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - // +listType=atomic - Permissions []PluginMetaV0alpha1RoleRolePermissions `json:"permissions,omitempty"` -} - -// NewPluginMetaV0alpha1RoleRole creates a new PluginMetaV0alpha1RoleRole object. -func NewPluginMetaV0alpha1RoleRole() *PluginMetaV0alpha1RoleRole { - return &PluginMetaV0alpha1RoleRole{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1ExtensionsAddedComponents struct { - // +listType=set - Targets []string `json:"targets"` - Title string `json:"title"` - Description *string `json:"description,omitempty"` -} - -// NewPluginMetaV0alpha1ExtensionsAddedComponents creates a new PluginMetaV0alpha1ExtensionsAddedComponents object. -func NewPluginMetaV0alpha1ExtensionsAddedComponents() *PluginMetaV0alpha1ExtensionsAddedComponents { - return &PluginMetaV0alpha1ExtensionsAddedComponents{ - Targets: []string{}, - } -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1ExtensionsAddedLinks struct { - // +listType=set - Targets []string `json:"targets"` - Title string `json:"title"` - Description *string `json:"description,omitempty"` -} - -// NewPluginMetaV0alpha1ExtensionsAddedLinks creates a new PluginMetaV0alpha1ExtensionsAddedLinks object. -func NewPluginMetaV0alpha1ExtensionsAddedLinks() *PluginMetaV0alpha1ExtensionsAddedLinks { - return &PluginMetaV0alpha1ExtensionsAddedLinks{ - Targets: []string{}, - } -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1ExtensionsExposedComponents struct { - Id string `json:"id"` - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` -} - -// NewPluginMetaV0alpha1ExtensionsExposedComponents creates a new PluginMetaV0alpha1ExtensionsExposedComponents object. -func NewPluginMetaV0alpha1ExtensionsExposedComponents() *PluginMetaV0alpha1ExtensionsExposedComponents { - return &PluginMetaV0alpha1ExtensionsExposedComponents{} -} - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1ExtensionsExtensionPoints struct { - Id string `json:"id"` - Title *string `json:"title,omitempty"` - Description *string `json:"description,omitempty"` -} - -// NewPluginMetaV0alpha1ExtensionsExtensionPoints creates a new PluginMetaV0alpha1ExtensionsExtensionPoints object. -func NewPluginMetaV0alpha1ExtensionsExtensionPoints() *PluginMetaV0alpha1ExtensionsExtensionPoints { - return &PluginMetaV0alpha1ExtensionsExtensionPoints{} -} - -// +k8s:openapi-gen=true -type PluginMetaJSONDataType string - -const ( - PluginMetaJSONDataTypeApp PluginMetaJSONDataType = "app" - PluginMetaJSONDataTypeDatasource PluginMetaJSONDataType = "datasource" - PluginMetaJSONDataTypePanel PluginMetaJSONDataType = "panel" - PluginMetaJSONDataTypeRenderer PluginMetaJSONDataType = "renderer" -) - -// +k8s:openapi-gen=true -type PluginMetaJSONDataCategory string - -const ( - PluginMetaJSONDataCategoryTsdb PluginMetaJSONDataCategory = "tsdb" - PluginMetaJSONDataCategoryLogging PluginMetaJSONDataCategory = "logging" - PluginMetaJSONDataCategoryCloud PluginMetaJSONDataCategory = "cloud" - PluginMetaJSONDataCategoryTracing PluginMetaJSONDataCategory = "tracing" - PluginMetaJSONDataCategoryProfiling PluginMetaJSONDataCategory = "profiling" - PluginMetaJSONDataCategorySql PluginMetaJSONDataCategory = "sql" - PluginMetaJSONDataCategoryEnterprise PluginMetaJSONDataCategory = "enterprise" - PluginMetaJSONDataCategoryIot PluginMetaJSONDataCategory = "iot" - PluginMetaJSONDataCategoryOther PluginMetaJSONDataCategory = "other" -) - -// +k8s:openapi-gen=true -type PluginMetaJSONDataState string - -const ( - PluginMetaJSONDataStateAlpha PluginMetaJSONDataState = "alpha" - PluginMetaJSONDataStateBeta PluginMetaJSONDataState = "beta" -) - -// +k8s:openapi-gen=true -type PluginMetaIncludeType string - -const ( - PluginMetaIncludeTypeDashboard PluginMetaIncludeType = "dashboard" - PluginMetaIncludeTypePage PluginMetaIncludeType = "page" - PluginMetaIncludeTypePanel PluginMetaIncludeType = "panel" - PluginMetaIncludeTypeDatasource PluginMetaIncludeType = "datasource" -) - -// +k8s:openapi-gen=true -type PluginMetaIncludeRole string - -const ( - PluginMetaIncludeRoleAdmin PluginMetaIncludeRole = "Admin" - PluginMetaIncludeRoleEditor PluginMetaIncludeRole = "Editor" - PluginMetaIncludeRoleViewer PluginMetaIncludeRole = "Viewer" -) - -// +k8s:openapi-gen=true -type PluginMetaV0alpha1DependenciesPluginsType string - -const ( - PluginMetaV0alpha1DependenciesPluginsTypeApp PluginMetaV0alpha1DependenciesPluginsType = "app" - PluginMetaV0alpha1DependenciesPluginsTypeDatasource PluginMetaV0alpha1DependenciesPluginsType = "datasource" - PluginMetaV0alpha1DependenciesPluginsTypePanel PluginMetaV0alpha1DependenciesPluginsType = "panel" -) diff --git a/apps/plugins/pkg/apis/plugins_manifest.go b/apps/plugins/pkg/apis/plugins_manifest.go index e09c70244a7..1a351eb2baf 100644 --- a/apps/plugins/pkg/apis/plugins_manifest.go +++ b/apps/plugins/pkg/apis/plugins_manifest.go @@ -20,12 +20,12 @@ import ( ) var ( - rawSchemaPluginv0alpha1 = []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"},"Plugin":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"class":{"enum":["core","external"],"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version","class"],"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"}}`) - versionSchemaPluginv0alpha1 app.VersionSchema - _ = json.Unmarshal(rawSchemaPluginv0alpha1, &versionSchemaPluginv0alpha1) - rawSchemaPluginMetav0alpha1 = []byte(`{"Dependencies":{"additionalProperties":false,"properties":{"extensions":{"additionalProperties":false,"properties":{"exposedComponents":{"description":"+listType=set","items":{"type":"string"},"type":"array"}},"type":"object"},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"enum":["app","datasource","panel"],"type":"string"}},"required":["id","type","name"],"type":"object"},"type":"array"}},"required":["grafanaDependency"],"type":"object"},"EnterpriseFeatures":{"additionalProperties":false,"properties":{"healthDiagnosticsErrors":{"default":false,"description":"Allow additional properties","type":"boolean"}},"type":"object"},"Extensions":{"additionalProperties":false,"properties":{"addedComponents":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"addedLinks":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"}},"type":"object"},"IAM":{"additionalProperties":false,"properties":{"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"Include":{"additionalProperties":false,"properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"enum":["Admin","Editor","Viewer"],"type":"string"},"type":{"enum":["dashboard","page","panel","datasource"],"type":"string"},"uid":{"type":"string"}},"type":"object"},"Info":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"description":"Optional fields","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","items":{"type":"string"},"type":"array"},"links":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"type":"array"},"logos":{"additionalProperties":false,"properties":{"large":{"type":"string"},"small":{"type":"string"}},"required":["small","large"],"type":"object"},"screenshots":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"path":{"type":"string"}},"type":"object"},"type":"array"},"updated":{"type":"string"},"version":{"type":"string"}},"required":["keywords","logos","updated","version"],"type":"object"},"JSONData":{"additionalProperties":false,"description":"JSON configuration schema for Grafana plugins\nConverted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json","properties":{"alerting":{"description":"Optional fields","type":"boolean"},"annotations":{"type":"boolean"},"autoEnabled":{"type":"boolean"},"backend":{"type":"boolean"},"buildMode":{"type":"string"},"builtIn":{"type":"boolean"},"category":{"enum":["tsdb","logging","cloud","tracing","profiling","sql","enterprise","iot","other"],"type":"string"},"dependencies":{"$ref":"#/components/schemas/Dependencies","description":"Dependency information"},"enterpriseFeatures":{"$ref":"#/components/schemas/EnterpriseFeatures"},"executable":{"type":"string"},"extensions":{"$ref":"#/components/schemas/Extensions"},"hideFromList":{"type":"boolean"},"iam":{"$ref":"#/components/schemas/IAM"},"id":{"description":"Unique name of the plugin","type":"string"},"includes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Include"},"type":"array"},"info":{"$ref":"#/components/schemas/Info","description":"Metadata for the plugin"},"logs":{"type":"boolean"},"metrics":{"type":"boolean"},"multiValueFilterOperators":{"type":"boolean"},"name":{"description":"Human-readable name of the plugin","type":"string"},"pascalName":{"type":"string"},"preload":{"type":"boolean"},"queryOptions":{"$ref":"#/components/schemas/QueryOptions"},"roles":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Role"},"type":"array"},"routes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Route"},"type":"array"},"skipDataQuery":{"type":"boolean"},"state":{"enum":["alpha","beta"],"type":"string"},"streaming":{"type":"boolean"},"suggestions":{"type":"boolean"},"tracing":{"type":"boolean"},"type":{"description":"Plugin type","enum":["app","datasource","panel","renderer"],"type":"string"}},"required":["id","type","name","info","dependencies"],"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"},"PluginMeta":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"QueryOptions":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"type":"object"},"Role":{"additionalProperties":false,"properties":{"grants":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"role":{"additionalProperties":false,"properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"}},"type":"object"},"Route":{"additionalProperties":false,"properties":{"body":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"headers":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"jwtTokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"content":{"type":"string"},"name":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"spec":{"additionalProperties":false,"properties":{"pluginJSON":{"$ref":"#/components/schemas/JSONData"}},"required":["pluginJSON"],"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"}}`) - versionSchemaPluginMetav0alpha1 app.VersionSchema - _ = json.Unmarshal(rawSchemaPluginMetav0alpha1, &versionSchemaPluginMetav0alpha1) + rawSchemaPluginv0alpha1 = []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"},"Plugin":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"class":{"enum":["core","external"],"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version","class"],"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"}}`) + versionSchemaPluginv0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaPluginv0alpha1, &versionSchemaPluginv0alpha1) + rawSchemaMetav0alpha1 = []byte(`{"Dependencies":{"additionalProperties":false,"properties":{"extensions":{"additionalProperties":false,"properties":{"exposedComponents":{"description":"+listType=set","items":{"type":"string"},"type":"array"}},"type":"object"},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"enum":["app","datasource","panel"],"type":"string"}},"required":["id","type","name"],"type":"object"},"type":"array"}},"required":["grafanaDependency"],"type":"object"},"EnterpriseFeatures":{"additionalProperties":false,"properties":{"healthDiagnosticsErrors":{"default":false,"description":"Allow additional properties","type":"boolean"}},"type":"object"},"Extensions":{"additionalProperties":false,"properties":{"addedComponents":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"addedLinks":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"}},"type":"object"},"IAM":{"additionalProperties":false,"properties":{"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"Include":{"additionalProperties":false,"properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"enum":["Admin","Editor","Viewer"],"type":"string"},"type":{"enum":["dashboard","page","panel","datasource"],"type":"string"},"uid":{"type":"string"}},"type":"object"},"Info":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"description":"Optional fields","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","items":{"type":"string"},"type":"array"},"links":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"type":"array"},"logos":{"additionalProperties":false,"properties":{"large":{"type":"string"},"small":{"type":"string"}},"required":["small","large"],"type":"object"},"screenshots":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"path":{"type":"string"}},"type":"object"},"type":"array"},"updated":{"type":"string"},"version":{"type":"string"}},"required":["keywords","logos","updated","version"],"type":"object"},"JSONData":{"additionalProperties":false,"description":"JSON configuration schema for Grafana plugins\nConverted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json","properties":{"alerting":{"description":"Optional fields","type":"boolean"},"annotations":{"type":"boolean"},"autoEnabled":{"type":"boolean"},"backend":{"type":"boolean"},"buildMode":{"type":"string"},"builtIn":{"type":"boolean"},"category":{"enum":["tsdb","logging","cloud","tracing","profiling","sql","enterprise","iot","other"],"type":"string"},"dependencies":{"$ref":"#/components/schemas/Dependencies","description":"Dependency information"},"enterpriseFeatures":{"$ref":"#/components/schemas/EnterpriseFeatures"},"executable":{"type":"string"},"extensions":{"$ref":"#/components/schemas/Extensions"},"hideFromList":{"type":"boolean"},"iam":{"$ref":"#/components/schemas/IAM"},"id":{"description":"Unique name of the plugin","type":"string"},"includes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Include"},"type":"array"},"info":{"$ref":"#/components/schemas/Info","description":"Metadata for the plugin"},"logs":{"type":"boolean"},"metrics":{"type":"boolean"},"multiValueFilterOperators":{"type":"boolean"},"name":{"description":"Human-readable name of the plugin","type":"string"},"pascalName":{"type":"string"},"preload":{"type":"boolean"},"queryOptions":{"$ref":"#/components/schemas/QueryOptions"},"roles":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Role"},"type":"array"},"routes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Route"},"type":"array"},"skipDataQuery":{"type":"boolean"},"state":{"enum":["alpha","beta"],"type":"string"},"streaming":{"type":"boolean"},"suggestions":{"type":"boolean"},"tracing":{"type":"boolean"},"type":{"description":"Plugin type","enum":["app","datasource","panel","renderer"],"type":"string"}},"required":["id","type","name","info","dependencies"],"type":"object"},"Meta":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"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"},"QueryOptions":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"type":"object"},"Role":{"additionalProperties":false,"properties":{"grants":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"role":{"additionalProperties":false,"properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"}},"type":"object"},"Route":{"additionalProperties":false,"properties":{"body":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"headers":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"jwtTokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"content":{"type":"string"},"name":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"spec":{"additionalProperties":false,"properties":{"pluginJSON":{"$ref":"#/components/schemas/JSONData"}},"required":["pluginJSON"],"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"}}`) + versionSchemaMetav0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaMetav0alpha1, &versionSchemaMetav0alpha1) ) var appManifestData = app.ManifestData{ @@ -46,11 +46,11 @@ var appManifestData = app.ManifestData{ }, { - Kind: "PluginMeta", - Plural: "PluginMetas", + Kind: "Meta", + Plural: "Metas", Scope: "Namespaced", Conversion: false, - Schema: &versionSchemaPluginMetav0alpha1, + Schema: &versionSchemaMetav0alpha1, }, }, Routes: app.ManifestVersionRoutes{ @@ -71,8 +71,8 @@ func RemoteManifest() app.Manifest { } var kindVersionToGoType = map[string]resource.Kind{ - "Plugin/v0alpha1": v0alpha1.PluginKind(), - "PluginMeta/v0alpha1": v0alpha1.PluginMetaKind(), + "Plugin/v0alpha1": v0alpha1.PluginKind(), + "Meta/v0alpha1": v0alpha1.MetaKind(), } // ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. diff --git a/apps/plugins/pkg/app/app.go b/apps/plugins/pkg/app/app.go index 3d17cc34221..c296bccd9e1 100644 --- a/apps/plugins/pkg/app/app.go +++ b/apps/plugins/pkg/app/app.go @@ -3,6 +3,7 @@ package app import ( "context" "fmt" + "sync" "github.com/grafana/grafana-app-sdk/app" "github.com/grafana/grafana-app-sdk/k8s" @@ -16,7 +17,6 @@ import ( restclient "k8s.io/client-go/rest" "k8s.io/klog/v2" - authlib "github.com/grafana/authlib/types" pluginsappapis "github.com/grafana/grafana/apps/plugins/pkg/apis" pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" "github.com/grafana/grafana/apps/plugins/pkg/app/meta" @@ -43,7 +43,7 @@ func New(cfg app.Config) (app.App, error) { Kind: pluginsv0alpha1.PluginKind(), }, { - Kind: pluginsv0alpha1.PluginMetaKind(), + Kind: pluginsv0alpha1.MetaKind(), }, }, } @@ -69,6 +69,7 @@ type PluginAppConfig struct { } func ProvideAppInstaller( + authorizer authorizer.Authorizer, metaProviderManager *meta.ProviderManager, ) (*PluginAppInstaller, error) { specificConfig := &PluginAppConfig{ @@ -87,31 +88,30 @@ func ProvideAppInstaller( appInstaller := &PluginAppInstaller{ AppInstaller: defaultInstaller, + authorizer: authorizer, metaManager: metaProviderManager, ready: make(chan struct{}), } return appInstaller, nil } -func (p *PluginAppInstaller) WithAccessChecker(access authlib.AccessChecker) *PluginAppInstaller { - p.access = access - return p -} - type PluginAppInstaller struct { appsdkapiserver.AppInstaller metaManager *meta.ProviderManager - access authlib.AccessChecker + authorizer authorizer.Authorizer // restConfig is set during InitializeApp and used by the client factory restConfig *restclient.Config ready chan struct{} + readyOnce sync.Once } func (p *PluginAppInstaller) InitializeApp(restConfig restclient.Config) error { if p.restConfig == nil { p.restConfig = &restConfig - close(p.ready) + p.readyOnce.Do(func() { + close(p.ready) + }) } return p.AppInstaller.InitializeApp(restConfig) } @@ -137,9 +137,9 @@ func (p *PluginAppInstaller) InstallAPIs( return client, nil } - pluginMetaGVR := pluginsv0alpha1.PluginMetaKind().GroupVersionResource() + pluginMetaGVR := pluginsv0alpha1.MetaKind().GroupVersionResource() replacedStorage := map[schema.GroupVersionResource]rest.Storage{ - pluginMetaGVR: NewPluginMetaStorage(p.metaManager, clientFactory), + pluginMetaGVR: NewMetaStorage(p.metaManager, clientFactory), } wrappedServer := &customStorageWrapper{ wrapped: server, @@ -149,34 +149,5 @@ func (p *PluginAppInstaller) InstallAPIs( } func (p *PluginAppInstaller) GetAuthorizer() authorizer.Authorizer { - if p.access == nil { - return nil - } - - return authorizer.AuthorizerFunc( - func(ctx context.Context, a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) { - info, ok := authlib.AuthInfoFrom(ctx) - if !ok { - return authorizer.DecisionDeny, "failed to get auth info", nil - } - - res, err := p.access.Check(ctx, info, authlib.CheckRequest{ - Verb: a.GetVerb(), - Group: a.GetAPIGroup(), - Resource: a.GetResource(), - Name: a.GetName(), - Namespace: a.GetNamespace(), - Subresource: a.GetSubresource(), - Path: a.GetPath(), - }, "") - if err != nil { - return authorizer.DecisionDeny, "failed to perform authorization", err - } - - if !res.Allowed { - return authorizer.DecisionDeny, "permission denied", nil - } - - return authorizer.DecisionAllow, "", nil - }) + return p.authorizer } diff --git a/apps/plugins/pkg/app/authorizer.go b/apps/plugins/pkg/app/authorizer.go deleted file mode 100644 index b6a800694f7..00000000000 --- a/apps/plugins/pkg/app/authorizer.go +++ /dev/null @@ -1,32 +0,0 @@ -package app - -import ( - "context" - - "k8s.io/apiserver/pkg/authorization/authorizer" - - "github.com/grafana/grafana/pkg/apimachinery/identity" -) - -func GetAuthorizer() authorizer.Authorizer { - return authorizer.AuthorizerFunc(func( - ctx context.Context, attr authorizer.Attributes, - ) (authorized authorizer.Decision, reason string, err error) { - if !attr.IsResourceRequest() { - return authorizer.DecisionNoOpinion, "", nil - } - - // require a user - u, err := identity.GetRequester(ctx) - if err != nil { - return authorizer.DecisionDeny, "valid user is required", err - } - - // check if is admin - if u.HasRole(identity.RoleAdmin) { - return authorizer.DecisionAllow, "", nil - } - - return authorizer.DecisionDeny, "forbidden", nil - }) -} diff --git a/apps/plugins/pkg/app/meta/cloud.go b/apps/plugins/pkg/app/meta/catalog.go similarity index 56% rename from apps/plugins/pkg/app/meta/cloud.go rename to apps/plugins/pkg/app/meta/catalog.go index f49445d093a..6e6a47fa0bd 100644 --- a/apps/plugins/pkg/app/meta/cloud.go +++ b/apps/plugins/pkg/app/meta/catalog.go @@ -15,29 +15,29 @@ import ( ) const ( - defaultCloudTTL = 1 * time.Hour + defaultCatalogTTL = 1 * time.Hour ) -// CloudProvider retrieves plugin metadata from the grafana.com API. -type CloudProvider struct { +// CatalogProvider retrieves plugin metadata from the grafana.com API. +type CatalogProvider struct { httpClient *http.Client grafanaComAPIURL string log logging.Logger ttl time.Duration } -// NewCloudProvider creates a new CloudProvider that fetches metadata from grafana.com. -func NewCloudProvider(grafanaComAPIURL string) *CloudProvider { - return NewCloudProviderWithTTL(grafanaComAPIURL, defaultCloudTTL) +// NewCatalogProvider creates a new CatalogProvider that fetches metadata from grafana.com. +func NewCatalogProvider(grafanaComAPIURL string) *CatalogProvider { + return NewCatalogProviderWithTTL(grafanaComAPIURL, defaultCatalogTTL) } -// NewCloudProviderWithTTL creates a new CloudProvider with a custom TTL. -func NewCloudProviderWithTTL(grafanaComAPIURL string, ttl time.Duration) *CloudProvider { +// NewCatalogProviderWithTTL creates a new CatalogProvider with a custom TTL. +func NewCatalogProviderWithTTL(grafanaComAPIURL string, ttl time.Duration) *CatalogProvider { if grafanaComAPIURL == "" { grafanaComAPIURL = "https://grafana.com/api/plugins" } - return &CloudProvider{ + return &CatalogProvider{ httpClient: &http.Client{ Timeout: 10 * time.Second, }, @@ -49,7 +49,7 @@ func NewCloudProviderWithTTL(grafanaComAPIURL string, ttl time.Duration) *CloudP // GetMeta fetches plugin metadata from grafana.com API endpoint: // GET /api/plugins/{pluginId}/versions/{version} -func (p *CloudProvider) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) { +func (p *CatalogProvider) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) { u, err := url.Parse(p.grafanaComAPIURL) if err != nil { return nil, fmt.Errorf("invalid grafana.com API URL: %w", err) @@ -96,24 +96,24 @@ func (p *CloudProvider) GetMeta(ctx context.Context, pluginID, version string) ( // grafanaComPluginVersionMeta represents the response from grafana.com API // GET /api/plugins/{pluginId}/versions/{version} type grafanaComPluginVersionMeta struct { - PluginID string `json:"pluginSlug"` - Version string `json:"version"` - URL string `json:"url"` - Commit string `json:"commit"` - Description string `json:"description"` - Keywords []string `json:"keywords"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - JSON pluginsv0alpha1.PluginMetaJSONData `json:"json"` - Readme string `json:"readme"` - Downloads int `json:"downloads"` - Verified bool `json:"verified"` - Status string `json:"status"` - StatusContext string `json:"statusContext"` - DownloadSlug string `json:"downloadSlug"` - SignatureType string `json:"signatureType"` - SignedByOrg string `json:"signedByOrg"` - SignedByOrgName string `json:"signedByOrgName"` + PluginID string `json:"pluginSlug"` + Version string `json:"version"` + URL string `json:"url"` + Commit string `json:"commit"` + Description string `json:"description"` + Keywords []string `json:"keywords"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + JSON pluginsv0alpha1.MetaJSONData `json:"json"` + Readme string `json:"readme"` + Downloads int `json:"downloads"` + Verified bool `json:"verified"` + Status string `json:"status"` + StatusContext string `json:"statusContext"` + DownloadSlug string `json:"downloadSlug"` + SignatureType string `json:"signatureType"` + SignedByOrg string `json:"signedByOrg"` + SignedByOrgName string `json:"signedByOrgName"` Packages struct { Any struct { Md5 string `json:"md5"` diff --git a/apps/plugins/pkg/app/meta/cloud_test.go b/apps/plugins/pkg/app/meta/catalog_test.go similarity index 79% rename from apps/plugins/pkg/app/meta/cloud_test.go rename to apps/plugins/pkg/app/meta/catalog_test.go index bef2d1a5041..845afc7cd54 100644 --- a/apps/plugins/pkg/app/meta/cloud_test.go +++ b/apps/plugins/pkg/app/meta/catalog_test.go @@ -15,14 +15,14 @@ import ( pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" ) -func TestCloudProvider_GetMeta(t *testing.T) { +func TestCatalogProvider_GetMeta(t *testing.T) { ctx := context.Background() t.Run("successfully fetches plugin metadata", func(t *testing.T) { - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + expectedMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -44,13 +44,13 @@ func TestCloudProvider_GetMeta(t *testing.T) { })) defer server.Close() - provider := NewCloudProvider(server.URL + "/api/plugins") + provider := NewCatalogProvider(server.URL + "/api/plugins") result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") require.NoError(t, err) require.NotNil(t, result) assert.Equal(t, expectedMeta, result.Meta) - assert.Equal(t, defaultCloudTTL, result.TTL) + assert.Equal(t, defaultCatalogTTL, result.TTL) }) t.Run("returns ErrMetaNotFound for 404 status", func(t *testing.T) { @@ -59,7 +59,7 @@ func TestCloudProvider_GetMeta(t *testing.T) { })) defer server.Close() - provider := NewCloudProvider(server.URL + "/api/plugins") + provider := NewCatalogProvider(server.URL + "/api/plugins") result, err := provider.GetMeta(ctx, "nonexistent-plugin", "1.0.0") assert.Error(t, err) @@ -73,7 +73,7 @@ func TestCloudProvider_GetMeta(t *testing.T) { })) defer server.Close() - provider := NewCloudProvider(server.URL + "/api/plugins") + provider := NewCatalogProvider(server.URL + "/api/plugins") result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") assert.Error(t, err) @@ -89,7 +89,7 @@ func TestCloudProvider_GetMeta(t *testing.T) { })) defer server.Close() - provider := NewCloudProvider(server.URL + "/api/plugins") + provider := NewCatalogProvider(server.URL + "/api/plugins") result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") assert.Error(t, err) @@ -98,7 +98,7 @@ func TestCloudProvider_GetMeta(t *testing.T) { }) t.Run("returns error for invalid API URL", func(t *testing.T) { - provider := NewCloudProvider("://invalid-url") + provider := NewCatalogProvider("://invalid-url") result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") assert.Error(t, err) @@ -108,10 +108,10 @@ func TestCloudProvider_GetMeta(t *testing.T) { t.Run("uses custom TTL when provided", func(t *testing.T) { customTTL := 2 * time.Hour - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + expectedMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -127,7 +127,7 @@ func TestCloudProvider_GetMeta(t *testing.T) { })) defer server.Close() - provider := NewCloudProviderWithTTL(server.URL+"/api/plugins", customTTL) + provider := NewCatalogProviderWithTTL(server.URL+"/api/plugins", customTTL) result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") require.NoError(t, err) @@ -145,7 +145,7 @@ func TestCloudProvider_GetMeta(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - provider := NewCloudProvider(server.URL + "/api/plugins") + provider := NewCatalogProvider(server.URL + "/api/plugins") result, err := provider.GetMeta(ctx, "test-plugin", "1.0.0") assert.Error(t, err) @@ -153,34 +153,34 @@ func TestCloudProvider_GetMeta(t *testing.T) { }) } -func TestNewCloudProvider(t *testing.T) { +func TestNewCatalogProvider(t *testing.T) { t.Run("creates provider with default TTL", func(t *testing.T) { - provider := NewCloudProvider("https://grafana.com/api/plugins") - assert.Equal(t, defaultCloudTTL, provider.ttl) + provider := NewCatalogProvider("https://grafana.com/api/plugins") + assert.Equal(t, defaultCatalogTTL, provider.ttl) assert.NotNil(t, provider.httpClient) assert.Equal(t, "https://grafana.com/api/plugins", provider.grafanaComAPIURL) }) t.Run("uses default URL when empty", func(t *testing.T) { - provider := NewCloudProvider("") + provider := NewCatalogProvider("") assert.Equal(t, "https://grafana.com/api/plugins", provider.grafanaComAPIURL) }) } -func TestNewCloudProviderWithTTL(t *testing.T) { +func TestNewCatalogProviderWithTTL(t *testing.T) { t.Run("creates provider with custom TTL", func(t *testing.T) { customTTL := 2 * time.Hour - provider := NewCloudProviderWithTTL("https://grafana.com/api/plugins", customTTL) + provider := NewCatalogProviderWithTTL("https://grafana.com/api/plugins", customTTL) assert.Equal(t, customTTL, provider.ttl) }) t.Run("accepts zero TTL", func(t *testing.T) { - provider := NewCloudProviderWithTTL("https://grafana.com/api/plugins", 0) + provider := NewCatalogProviderWithTTL("https://grafana.com/api/plugins", 0) assert.Equal(t, time.Duration(0), provider.ttl) }) t.Run("uses default URL when empty", func(t *testing.T) { - provider := NewCloudProviderWithTTL("", defaultCloudTTL) + provider := NewCatalogProviderWithTTL("", defaultCatalogTTL) assert.Equal(t, "https://grafana.com/api/plugins", provider.grafanaComAPIURL) }) } diff --git a/apps/plugins/pkg/app/meta/core.go b/apps/plugins/pkg/app/meta/core.go index 3452f95b24b..e16f7a23db5 100644 --- a/apps/plugins/pkg/app/meta/core.go +++ b/apps/plugins/pkg/app/meta/core.go @@ -23,7 +23,7 @@ const ( // CoreProvider retrieves plugin metadata for core plugins. type CoreProvider struct { mu sync.RWMutex - loadedPlugins map[string]pluginsv0alpha1.PluginMetaJSONData + loadedPlugins map[string]pluginsv0alpha1.MetaJSONData initialized bool ttl time.Duration } @@ -36,7 +36,7 @@ func NewCoreProvider() *CoreProvider { // NewCoreProviderWithTTL creates a new CoreProvider with a custom TTL. func NewCoreProviderWithTTL(ttl time.Duration) *CoreProvider { return &CoreProvider{ - loadedPlugins: make(map[string]pluginsv0alpha1.PluginMetaJSONData), + loadedPlugins: make(map[string]pluginsv0alpha1.MetaJSONData), ttl: ttl, } } @@ -119,17 +119,17 @@ func (p *CoreProvider) loadPlugins(ctx context.Context) error { } for _, bundle := range ps { - meta := jsonDataToPluginMetaJSONData(bundle.Primary.JSONData) + meta := jsonDataToMetaJSONData(bundle.Primary.JSONData) p.loadedPlugins[bundle.Primary.JSONData.ID] = meta } return nil } -// jsonDataToPluginMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.PluginMetaJSONData. +// jsonDataToMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.MetaJSONData. // nolint:gocyclo -func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.PluginMetaJSONData { - meta := pluginsv0alpha1.PluginMetaJSONData{ +func jsonDataToMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.MetaJSONData { + meta := pluginsv0alpha1.MetaJSONData{ Id: jsonData.ID, Name: jsonData.Name, } @@ -137,19 +137,19 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu // Map plugin type switch jsonData.Type { case plugins.TypeApp: - meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeApp + meta.Type = pluginsv0alpha1.MetaJSONDataTypeApp case plugins.TypeDataSource: - meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeDatasource + meta.Type = pluginsv0alpha1.MetaJSONDataTypeDatasource case plugins.TypePanel: - meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypePanel + meta.Type = pluginsv0alpha1.MetaJSONDataTypePanel case plugins.TypeRenderer: - meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeRenderer + meta.Type = pluginsv0alpha1.MetaJSONDataTypeRenderer } // Map Info - meta.Info = pluginsv0alpha1.PluginMetaInfo{ + meta.Info = pluginsv0alpha1.MetaInfo{ Keywords: jsonData.Info.Keywords, - Logos: pluginsv0alpha1.PluginMetaV0alpha1InfoLogos{ + Logos: pluginsv0alpha1.MetaV0alpha1InfoLogos{ Small: jsonData.Info.Logos.Small, Large: jsonData.Info.Logos.Large, }, @@ -162,7 +162,7 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu } if jsonData.Info.Author.Name != "" || jsonData.Info.Author.URL != "" { - author := &pluginsv0alpha1.PluginMetaV0alpha1InfoAuthor{} + author := &pluginsv0alpha1.MetaV0alpha1InfoAuthor{} if jsonData.Info.Author.Name != "" { author.Name = &jsonData.Info.Author.Name } @@ -173,9 +173,9 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu } if len(jsonData.Info.Links) > 0 { - meta.Info.Links = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links)) + meta.Info.Links = make([]pluginsv0alpha1.MetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links)) for _, link := range jsonData.Info.Links { - v0Link := pluginsv0alpha1.PluginMetaV0alpha1InfoLinks{} + v0Link := pluginsv0alpha1.MetaV0alpha1InfoLinks{} if link.Name != "" { v0Link.Name = &link.Name } @@ -187,9 +187,9 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu } if len(jsonData.Info.Screenshots) > 0 { - meta.Info.Screenshots = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots)) + meta.Info.Screenshots = make([]pluginsv0alpha1.MetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots)) for _, screenshot := range jsonData.Info.Screenshots { - v0Screenshot := pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots{} + v0Screenshot := pluginsv0alpha1.MetaV0alpha1InfoScreenshots{} if screenshot.Name != "" { v0Screenshot.Name = &screenshot.Name } @@ -201,7 +201,7 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu } // Map Dependencies - meta.Dependencies = pluginsv0alpha1.PluginMetaDependencies{ + meta.Dependencies = pluginsv0alpha1.MetaDependencies{ GrafanaDependency: jsonData.Dependencies.GrafanaDependency, } @@ -210,18 +210,18 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu } if len(jsonData.Dependencies.Plugins) > 0 { - meta.Dependencies.Plugins = make([]pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins)) + meta.Dependencies.Plugins = make([]pluginsv0alpha1.MetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins)) for _, dep := range jsonData.Dependencies.Plugins { - var depType pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsType + var depType pluginsv0alpha1.MetaV0alpha1DependenciesPluginsType switch dep.Type { case "app": - depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeApp + depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypeApp case "datasource": - depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeDatasource + depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypeDatasource case "panel": - depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypePanel + depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypePanel } - meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins{ + meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.MetaV0alpha1DependenciesPlugins{ Id: dep.ID, Type: depType, Name: dep.Name, @@ -230,7 +230,7 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu } if len(jsonData.Dependencies.Extensions.ExposedComponents) > 0 { - meta.Dependencies.Extensions = &pluginsv0alpha1.PluginMetaV0alpha1DependenciesExtensions{ + meta.Dependencies.Extensions = &pluginsv0alpha1.MetaV0alpha1DependenciesExtensions{ ExposedComponents: jsonData.Dependencies.Extensions.ExposedComponents, } } @@ -278,40 +278,40 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu // Map category if jsonData.Category != "" { - var category pluginsv0alpha1.PluginMetaJSONDataCategory + var category pluginsv0alpha1.MetaJSONDataCategory switch jsonData.Category { case "tsdb": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryTsdb + category = pluginsv0alpha1.MetaJSONDataCategoryTsdb case "logging": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryLogging + category = pluginsv0alpha1.MetaJSONDataCategoryLogging case "cloud": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryCloud + category = pluginsv0alpha1.MetaJSONDataCategoryCloud case "tracing": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryTracing + category = pluginsv0alpha1.MetaJSONDataCategoryTracing case "profiling": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryProfiling + category = pluginsv0alpha1.MetaJSONDataCategoryProfiling case "sql": - category = pluginsv0alpha1.PluginMetaJSONDataCategorySql + category = pluginsv0alpha1.MetaJSONDataCategorySql case "enterprise": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryEnterprise + category = pluginsv0alpha1.MetaJSONDataCategoryEnterprise case "iot": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryIot + category = pluginsv0alpha1.MetaJSONDataCategoryIot case "other": - category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther + category = pluginsv0alpha1.MetaJSONDataCategoryOther default: - category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther + category = pluginsv0alpha1.MetaJSONDataCategoryOther } meta.Category = &category } // Map state if jsonData.State != "" { - var state pluginsv0alpha1.PluginMetaJSONDataState + var state pluginsv0alpha1.MetaJSONDataState switch jsonData.State { case plugins.ReleaseStateAlpha: - state = pluginsv0alpha1.PluginMetaJSONDataStateAlpha + state = pluginsv0alpha1.MetaJSONDataStateAlpha case plugins.ReleaseStateBeta: - state = pluginsv0alpha1.PluginMetaJSONDataStateBeta + state = pluginsv0alpha1.MetaJSONDataStateBeta default: } if state != "" { @@ -326,7 +326,7 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu // Map QueryOptions if len(jsonData.QueryOptions) > 0 { - queryOptions := &pluginsv0alpha1.PluginMetaQueryOptions{} + queryOptions := &pluginsv0alpha1.MetaQueryOptions{} if val, ok := jsonData.QueryOptions["maxDataPoints"]; ok { queryOptions.MaxDataPoints = &val } @@ -341,23 +341,23 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu // Map Includes if len(jsonData.Includes) > 0 { - meta.Includes = make([]pluginsv0alpha1.PluginMetaInclude, 0, len(jsonData.Includes)) + meta.Includes = make([]pluginsv0alpha1.MetaInclude, 0, len(jsonData.Includes)) for _, include := range jsonData.Includes { - v0Include := pluginsv0alpha1.PluginMetaInclude{} + v0Include := pluginsv0alpha1.MetaInclude{} if include.UID != "" { v0Include.Uid = &include.UID } if include.Type != "" { - var includeType pluginsv0alpha1.PluginMetaIncludeType + var includeType pluginsv0alpha1.MetaIncludeType switch include.Type { case "dashboard": - includeType = pluginsv0alpha1.PluginMetaIncludeTypeDashboard + includeType = pluginsv0alpha1.MetaIncludeTypeDashboard case "page": - includeType = pluginsv0alpha1.PluginMetaIncludeTypePage + includeType = pluginsv0alpha1.MetaIncludeTypePage case "panel": - includeType = pluginsv0alpha1.PluginMetaIncludeTypePanel + includeType = pluginsv0alpha1.MetaIncludeTypePanel case "datasource": - includeType = pluginsv0alpha1.PluginMetaIncludeTypeDatasource + includeType = pluginsv0alpha1.MetaIncludeTypeDatasource } v0Include.Type = &includeType } @@ -368,14 +368,14 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu v0Include.Component = &include.Component } if include.Role != "" { - var role pluginsv0alpha1.PluginMetaIncludeRole + var role pluginsv0alpha1.MetaIncludeRole switch include.Role { case "Admin": - role = pluginsv0alpha1.PluginMetaIncludeRoleAdmin + role = pluginsv0alpha1.MetaIncludeRoleAdmin case "Editor": - role = pluginsv0alpha1.PluginMetaIncludeRoleEditor + role = pluginsv0alpha1.MetaIncludeRoleEditor case "Viewer": - role = pluginsv0alpha1.PluginMetaIncludeRoleViewer + role = pluginsv0alpha1.MetaIncludeRoleViewer } v0Include.Role = &role } @@ -400,9 +400,9 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu // Map Routes if len(jsonData.Routes) > 0 { - meta.Routes = make([]pluginsv0alpha1.PluginMetaRoute, 0, len(jsonData.Routes)) + meta.Routes = make([]pluginsv0alpha1.MetaRoute, 0, len(jsonData.Routes)) for _, route := range jsonData.Routes { - v0Route := pluginsv0alpha1.PluginMetaRoute{} + v0Route := pluginsv0alpha1.MetaRoute{} if route.Path != "" { v0Route.Path = &route.Path } @@ -427,9 +427,9 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu v0Route.Headers = headers } if len(route.URLParams) > 0 { - v0Route.UrlParams = make([]pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams, 0, len(route.URLParams)) + v0Route.UrlParams = make([]pluginsv0alpha1.MetaV0alpha1RouteUrlParams, 0, len(route.URLParams)) for _, param := range route.URLParams { - v0Param := pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams{} + v0Param := pluginsv0alpha1.MetaV0alpha1RouteUrlParams{} if param.Name != "" { v0Param.Name = ¶m.Name } @@ -440,7 +440,7 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu } } if route.TokenAuth != nil { - v0Route.TokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteTokenAuth{} + v0Route.TokenAuth = &pluginsv0alpha1.MetaV0alpha1RouteTokenAuth{} if route.TokenAuth.Url != "" { v0Route.TokenAuth.Url = &route.TokenAuth.Url } @@ -455,7 +455,7 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu } } if route.JwtTokenAuth != nil { - v0Route.JwtTokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteJwtTokenAuth{} + v0Route.JwtTokenAuth = &pluginsv0alpha1.MetaV0alpha1RouteJwtTokenAuth{} if route.JwtTokenAuth.Url != "" { v0Route.JwtTokenAuth.Url = &route.JwtTokenAuth.Url } @@ -482,12 +482,12 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu // Map Extensions if len(jsonData.Extensions.AddedLinks) > 0 || len(jsonData.Extensions.AddedComponents) > 0 || len(jsonData.Extensions.ExposedComponents) > 0 || len(jsonData.Extensions.ExtensionPoints) > 0 { - extensions := &pluginsv0alpha1.PluginMetaExtensions{} + extensions := &pluginsv0alpha1.MetaExtensions{} if len(jsonData.Extensions.AddedLinks) > 0 { - extensions.AddedLinks = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks)) + extensions.AddedLinks = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks)) for _, link := range jsonData.Extensions.AddedLinks { - v0Link := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks{ + v0Link := pluginsv0alpha1.MetaV0alpha1ExtensionsAddedLinks{ Targets: link.Targets, Title: link.Title, } @@ -499,9 +499,9 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu } if len(jsonData.Extensions.AddedComponents) > 0 { - extensions.AddedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents)) + extensions.AddedComponents = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents)) for _, comp := range jsonData.Extensions.AddedComponents { - v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents{ + v0Comp := pluginsv0alpha1.MetaV0alpha1ExtensionsAddedComponents{ Targets: comp.Targets, Title: comp.Title, } @@ -513,9 +513,9 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu } if len(jsonData.Extensions.ExposedComponents) > 0 { - extensions.ExposedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents)) + extensions.ExposedComponents = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents)) for _, comp := range jsonData.Extensions.ExposedComponents { - v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents{ + v0Comp := pluginsv0alpha1.MetaV0alpha1ExtensionsExposedComponents{ Id: comp.Id, } if comp.Title != "" { @@ -529,9 +529,9 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu } if len(jsonData.Extensions.ExtensionPoints) > 0 { - extensions.ExtensionPoints = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints)) + extensions.ExtensionPoints = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints)) for _, point := range jsonData.Extensions.ExtensionPoints { - v0Point := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints{ + v0Point := pluginsv0alpha1.MetaV0alpha1ExtensionsExtensionPoints{ Id: point.Id, } if point.Title != "" { @@ -549,13 +549,13 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu // Map Roles if len(jsonData.Roles) > 0 { - meta.Roles = make([]pluginsv0alpha1.PluginMetaRole, 0, len(jsonData.Roles)) + meta.Roles = make([]pluginsv0alpha1.MetaRole, 0, len(jsonData.Roles)) for _, role := range jsonData.Roles { - v0Role := pluginsv0alpha1.PluginMetaRole{ + v0Role := pluginsv0alpha1.MetaRole{ Grants: role.Grants, } if role.Role.Name != "" || role.Role.Description != "" || len(role.Role.Permissions) > 0 { - v0RoleRole := &pluginsv0alpha1.PluginMetaV0alpha1RoleRole{} + v0RoleRole := &pluginsv0alpha1.MetaV0alpha1RoleRole{} if role.Role.Name != "" { v0RoleRole.Name = &role.Role.Name } @@ -563,9 +563,9 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu v0RoleRole.Description = &role.Role.Description } if len(role.Role.Permissions) > 0 { - v0RoleRole.Permissions = make([]pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions)) + v0RoleRole.Permissions = make([]pluginsv0alpha1.MetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions)) for _, perm := range role.Role.Permissions { - v0Perm := pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions{} + v0Perm := pluginsv0alpha1.MetaV0alpha1RoleRolePermissions{} if perm.Action != "" { v0Perm.Action = &perm.Action } @@ -583,11 +583,11 @@ func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.Plu // Map IAM if jsonData.IAM != nil && len(jsonData.IAM.Permissions) > 0 { - iam := &pluginsv0alpha1.PluginMetaIAM{ - Permissions: make([]pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)), + iam := &pluginsv0alpha1.MetaIAM{ + Permissions: make([]pluginsv0alpha1.MetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)), } for _, perm := range jsonData.IAM.Permissions { - v0Perm := pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions{} + v0Perm := pluginsv0alpha1.MetaV0alpha1IAMPermissions{} if perm.Action != "" { v0Perm.Action = &perm.Action } diff --git a/apps/plugins/pkg/app/meta/core_test.go b/apps/plugins/pkg/app/meta/core_test.go index a9d7103e4a2..d5235c9d120 100644 --- a/apps/plugins/pkg/app/meta/core_test.go +++ b/apps/plugins/pkg/app/meta/core_test.go @@ -22,10 +22,10 @@ func TestCoreProvider_GetMeta(t *testing.T) { t.Run("returns cached plugin when available", func(t *testing.T) { provider := NewCoreProvider() - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + expectedMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } provider.mu.Lock() @@ -58,10 +58,10 @@ func TestCoreProvider_GetMeta(t *testing.T) { t.Run("ignores version parameter", func(t *testing.T) { provider := NewCoreProvider() - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + expectedMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } provider.mu.Lock() @@ -81,10 +81,10 @@ func TestCoreProvider_GetMeta(t *testing.T) { customTTL := 2 * time.Hour provider := NewCoreProviderWithTTL(customTTL) - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + expectedMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } provider.mu.Lock() @@ -271,11 +271,11 @@ func TestJsonDataToMeta(t *testing.T) { }, } - meta := jsonDataToPluginMetaJSONData(jsonData) + meta := jsonDataToMetaJSONData(jsonData) assert.Equal(t, "test-plugin", meta.Id) assert.Equal(t, "Test Plugin", meta.Name) - assert.Equal(t, pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, meta.Type) + assert.Equal(t, pluginsv0alpha1.MetaJSONDataTypeDatasource, meta.Type) assert.Equal(t, "1.0.0", meta.Info.Version) assert.Equal(t, "Test description", *meta.Info.Description) assert.Equal(t, []string{"test", "plugin"}, meta.Info.Keywords) @@ -293,7 +293,7 @@ func TestJsonDataToMeta(t *testing.T) { }, } - meta := jsonDataToPluginMetaJSONData(jsonData) + meta := jsonDataToMetaJSONData(jsonData) assert.Nil(t, meta.Info.Description) assert.Nil(t, meta.Info.Author) diff --git a/apps/plugins/pkg/app/meta/manager.go b/apps/plugins/pkg/app/meta/manager.go index e694e88bba7..7e99d0cc197 100644 --- a/apps/plugins/pkg/app/meta/manager.go +++ b/apps/plugins/pkg/app/meta/manager.go @@ -16,7 +16,7 @@ const ( // cachedMeta represents a cached metadata entry with expiration time type cachedMeta struct { - meta pluginsv0alpha1.PluginMetaJSONData + meta pluginsv0alpha1.MetaJSONData ttl time.Duration expiresAt time.Time } diff --git a/apps/plugins/pkg/app/meta/manager_test.go b/apps/plugins/pkg/app/meta/manager_test.go index 3d3fe3c936c..31a75424a23 100644 --- a/apps/plugins/pkg/app/meta/manager_test.go +++ b/apps/plugins/pkg/app/meta/manager_test.go @@ -35,10 +35,10 @@ func TestProviderManager_GetMeta(t *testing.T) { ctx := context.Background() t.Run("returns cached result when available and not expired", func(t *testing.T) { - cachedMeta := pluginsv0alpha1.PluginMetaJSONData{ + cachedMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } provider := &mockProvider{ @@ -60,7 +60,7 @@ func TestProviderManager_GetMeta(t *testing.T) { provider.getMetaFunc = func(ctx context.Context, pluginID, version string) (*Result, error) { return &Result{ - Meta: pluginsv0alpha1.PluginMetaJSONData{Id: "different"}, + Meta: pluginsv0alpha1.MetaJSONData{Id: "different"}, TTL: time.Hour, }, nil } @@ -73,10 +73,10 @@ func TestProviderManager_GetMeta(t *testing.T) { }) t.Run("fetches from provider when not cached", func(t *testing.T) { - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + expectedMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } expectedTTL := 2 * time.Hour @@ -108,15 +108,15 @@ func TestProviderManager_GetMeta(t *testing.T) { }) t.Run("does not cache result with zero TTL and tries next provider", func(t *testing.T) { - zeroTTLMeta := pluginsv0alpha1.PluginMetaJSONData{ + zeroTTLMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Zero TTL Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + expectedMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } provider1 := &mockProvider{ @@ -154,10 +154,10 @@ func TestProviderManager_GetMeta(t *testing.T) { }) t.Run("tries next provider when first returns ErrMetaNotFound", func(t *testing.T) { - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + expectedMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } provider1 := &mockProvider{ @@ -229,15 +229,15 @@ func TestProviderManager_GetMeta(t *testing.T) { }) t.Run("skips expired cache entries", func(t *testing.T) { - expiredMeta := pluginsv0alpha1.PluginMetaJSONData{ + expiredMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Expired Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } - expectedMeta := pluginsv0alpha1.PluginMetaJSONData{ + expectedMeta := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Test Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } callCount := 0 @@ -272,15 +272,15 @@ func TestProviderManager_GetMeta(t *testing.T) { }) t.Run("uses first successful provider", func(t *testing.T) { - expectedMeta1 := pluginsv0alpha1.PluginMetaJSONData{ + expectedMeta1 := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Provider 1 Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } - expectedMeta2 := pluginsv0alpha1.PluginMetaJSONData{ + expectedMeta2 := pluginsv0alpha1.MetaJSONData{ Id: "test-plugin", Name: "Provider 2 Plugin", - Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource, + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, } provider1 := &mockProvider{ @@ -331,9 +331,9 @@ func TestProviderManager_Run(t *testing.T) { func TestProviderManager_cleanupExpired(t *testing.T) { t.Run("removes expired entries", func(t *testing.T) { - validMeta := pluginsv0alpha1.PluginMetaJSONData{Id: "valid"} - expiredMeta1 := pluginsv0alpha1.PluginMetaJSONData{Id: "expired1"} - expiredMeta2 := pluginsv0alpha1.PluginMetaJSONData{Id: "expired2"} + validMeta := pluginsv0alpha1.MetaJSONData{Id: "valid"} + expiredMeta1 := pluginsv0alpha1.MetaJSONData{Id: "expired1"} + expiredMeta2 := pluginsv0alpha1.MetaJSONData{Id: "expired2"} provider := &mockProvider{ getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { diff --git a/apps/plugins/pkg/app/meta/provider.go b/apps/plugins/pkg/app/meta/provider.go index 8b5d8b0fcbd..818c0da9dc5 100644 --- a/apps/plugins/pkg/app/meta/provider.go +++ b/apps/plugins/pkg/app/meta/provider.go @@ -14,14 +14,14 @@ var ( // Result contains plugin metadata along with its recommended TTL. type Result struct { - Meta pluginsv0alpha1.PluginMetaJSONData + Meta pluginsv0alpha1.MetaJSONData TTL time.Duration } // Provider is used for retrieving plugin metadata. type Provider interface { // GetMeta retrieves plugin metadata for the given plugin ID and version. - // Returns the Result containing the PluginMetaJSONData and its recommended TTL. + // Returns the Result containing the MetaJSONData and its recommended TTL. // If the plugin is not found, returns ErrMetaNotFound. GetMeta(ctx context.Context, pluginID, version string) (*Result, error) } diff --git a/apps/plugins/pkg/app/storage.go b/apps/plugins/pkg/app/storage.go index 575b1ca21c8..f2dd2ac98d5 100644 --- a/apps/plugins/pkg/app/storage.go +++ b/apps/plugins/pkg/app/storage.go @@ -22,15 +22,15 @@ import ( ) var ( - _ rest.Scoper = (*PluginMetaStorage)(nil) - _ rest.SingularNameProvider = (*PluginMetaStorage)(nil) - _ rest.Getter = (*PluginMetaStorage)(nil) - _ rest.Lister = (*PluginMetaStorage)(nil) - _ rest.Storage = (*PluginMetaStorage)(nil) - _ rest.TableConvertor = (*PluginMetaStorage)(nil) + _ rest.Scoper = (*MetaStorage)(nil) + _ rest.SingularNameProvider = (*MetaStorage)(nil) + _ rest.Getter = (*MetaStorage)(nil) + _ rest.Lister = (*MetaStorage)(nil) + _ rest.Storage = (*MetaStorage)(nil) + _ rest.TableConvertor = (*MetaStorage)(nil) ) -type PluginMetaStorage struct { +type MetaStorage struct { metaManager *meta.ProviderManager client *pluginsv0alpha1.PluginClient clientFactory func(context.Context) (*pluginsv0alpha1.PluginClient, error) @@ -41,16 +41,16 @@ type PluginMetaStorage struct { tableConverter rest.TableConvertor } -func NewPluginMetaStorage( +func NewMetaStorage( metaManager *meta.ProviderManager, clientFactory func(context.Context) (*pluginsv0alpha1.PluginClient, error), -) *PluginMetaStorage { +) *MetaStorage { gr := schema.GroupResource{ Group: pluginsv0alpha1.APIGroup, - Resource: strings.ToLower(pluginsv0alpha1.PluginMetaKind().Plural()), + Resource: strings.ToLower(pluginsv0alpha1.MetaKind().Plural()), } - return &PluginMetaStorage{ + return &MetaStorage{ metaManager: metaManager, clientFactory: clientFactory, gr: gr, @@ -58,7 +58,7 @@ func NewPluginMetaStorage( } } -func (s *PluginMetaStorage) getClient(ctx context.Context) (*pluginsv0alpha1.PluginClient, error) { +func (s *MetaStorage) getClient(ctx context.Context) (*pluginsv0alpha1.PluginClient, error) { s.clientOnce.Do(func() { client, err := s.clientFactory(ctx) if err != nil { @@ -72,29 +72,29 @@ func (s *PluginMetaStorage) getClient(ctx context.Context) (*pluginsv0alpha1.Plu return s.client, s.clientErr } -func (s *PluginMetaStorage) New() runtime.Object { - return pluginsv0alpha1.PluginMetaKind().ZeroValue() +func (s *MetaStorage) New() runtime.Object { + return pluginsv0alpha1.MetaKind().ZeroValue() } -func (s *PluginMetaStorage) Destroy() {} +func (s *MetaStorage) Destroy() {} -func (s *PluginMetaStorage) NamespaceScoped() bool { +func (s *MetaStorage) NamespaceScoped() bool { return true } -func (s *PluginMetaStorage) GetSingularName() string { - return strings.ToLower(pluginsv0alpha1.PluginMetaKind().Kind()) +func (s *MetaStorage) GetSingularName() string { + return strings.ToLower(pluginsv0alpha1.MetaKind().Kind()) } -func (s *PluginMetaStorage) NewList() runtime.Object { - return pluginsv0alpha1.PluginMetaKind().ZeroListValue() +func (s *MetaStorage) NewList() runtime.Object { + return pluginsv0alpha1.MetaKind().ZeroListValue() } -func (s *PluginMetaStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { +func (s *MetaStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { return s.tableConverter.ConvertToTable(ctx, object, tableOptions) } -func (s *PluginMetaStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { +func (s *MetaStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -111,8 +111,8 @@ func (s *PluginMetaStorage) List(ctx context.Context, options *internalversion.L return nil, apierrors.NewInternalError(fmt.Errorf("failed to list plugins: %w", err)) } - // Convert each Plugin to PluginMeta - metaItems := make([]pluginsv0alpha1.PluginMeta, 0, len(plugins.Items)) + // Convert each Plugin to Meta + metaItems := make([]pluginsv0alpha1.Meta, 0, len(plugins.Items)) for _, plugin := range plugins.Items { result, err := s.metaManager.GetMeta(ctx, plugin.Spec.Id, plugin.Spec.Version) if err != nil { @@ -121,14 +121,14 @@ func (s *PluginMetaStorage) List(ctx context.Context, options *internalversion.L continue } - pluginMeta := createPluginMetaFromPluginMetaJSONData(result.Meta, plugin.Name, plugin.Namespace) + pluginMeta := createMetaFromMetaJSONData(result.Meta, plugin.Name, plugin.Namespace) metaItems = append(metaItems, *pluginMeta) } - list := &pluginsv0alpha1.PluginMetaList{ + list := &pluginsv0alpha1.MetaList{ TypeMeta: metav1.TypeMeta{ APIVersion: pluginsv0alpha1.APIGroup + "/" + pluginsv0alpha1.APIVersion, - Kind: pluginsv0alpha1.PluginMetaKind().Kind() + "List", + Kind: pluginsv0alpha1.MetaKind().Kind() + "List", }, Items: metaItems, } @@ -136,7 +136,7 @@ func (s *PluginMetaStorage) List(ctx context.Context, options *internalversion.L return list, nil } -func (s *PluginMetaStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { +func (s *MetaStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -169,17 +169,17 @@ func (s *PluginMetaStorage) Get(ctx context.Context, name string, options *metav return nil, apierrors.NewInternalError(fmt.Errorf("failed to fetch plugin metadata: %w", err)) } - return createPluginMetaFromPluginMetaJSONData(result.Meta, name, ns.Value), nil + return createMetaFromMetaJSONData(result.Meta, name, ns.Value), nil } -// createPluginMetaFromPluginMetaJSONData creates a PluginMeta k8s object from PluginMetaJSONData and plugin metadata. -func createPluginMetaFromPluginMetaJSONData(pluginJSON pluginsv0alpha1.PluginMetaJSONData, name, namespace string) *pluginsv0alpha1.PluginMeta { - pluginMeta := &pluginsv0alpha1.PluginMeta{ +// createMetaFromMetaJSONData creates a Meta k8s object from MetaJSONData and plugin metadata. +func createMetaFromMetaJSONData(pluginJSON pluginsv0alpha1.MetaJSONData, name, namespace string) *pluginsv0alpha1.Meta { + pluginMeta := &pluginsv0alpha1.Meta{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: pluginsv0alpha1.PluginMetaSpec{ + Spec: pluginsv0alpha1.MetaSpec{ PluginJSON: pluginJSON, }, } @@ -188,7 +188,7 @@ func createPluginMetaFromPluginMetaJSONData(pluginJSON pluginsv0alpha1.PluginMet pluginMeta.SetGroupVersionKind(schema.GroupVersionKind{ Group: pluginsv0alpha1.APIGroup, Version: pluginsv0alpha1.APIVersion, - Kind: pluginsv0alpha1.PluginMetaKind().Kind(), + Kind: pluginsv0alpha1.MetaKind().Kind(), }) return pluginMeta diff --git a/apps/quotas/Makefile b/apps/quotas/Makefile new file mode 100644 index 00000000000..230bfd4149a --- /dev/null +++ b/apps/quotas/Makefile @@ -0,0 +1,9 @@ +include ../sdk.mk + +.PHONY: generate # Run Grafana App SDK code generation +generate: install-app-sdk update-app-sdk + @$(APP_SDK_BIN) generate \ + --source=./kinds/ \ + --gogenpath=./pkg/apis \ + --grouping=group \ + --defencoding=none \ No newline at end of file diff --git a/apps/quotas/go.mod b/apps/quotas/go.mod new file mode 100644 index 00000000000..1ab18fe8876 --- /dev/null +++ b/apps/quotas/go.mod @@ -0,0 +1,92 @@ +module github.com/grafana/grafana/apps/quotas + +go 1.25.3 + +require ( + github.com/grafana/grafana-app-sdk v0.48.5 + github.com/grafana/grafana-app-sdk/logging v0.48.3 + k8s.io/apimachinery v0.34.2 + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-test/deep v1.1.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/onsi/ginkgo/v2 v2.22.2 // indirect + github.com/onsi/gomega v1.36.2 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.14.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.34.2 // indirect + k8s.io/apiextensions-apiserver v0.34.2 // indirect + k8s.io/client-go v0.34.2 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/apps/quotas/go.sum b/apps/quotas/go.sum new file mode 100644 index 00000000000..5787fa55023 --- /dev/null +++ b/apps/quotas/go.sum @@ -0,0 +1,252 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= +github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM= +github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= +github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= +k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= +k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= +k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= +k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= +k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= +k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/quotas/kinds/cue.mod/module.cue b/apps/quotas/kinds/cue.mod/module.cue new file mode 100644 index 00000000000..6e70f424d2e --- /dev/null +++ b/apps/quotas/kinds/cue.mod/module.cue @@ -0,0 +1,2 @@ +module: "github.com/grafana/grafana/apps/quotas/kinds" +language: version: "v0.8.2" diff --git a/apps/quotas/kinds/manifest.cue b/apps/quotas/kinds/manifest.cue new file mode 100644 index 00000000000..8f799ff070c --- /dev/null +++ b/apps/quotas/kinds/manifest.cue @@ -0,0 +1,92 @@ +package kinds + +manifest: { + // appName is the unique name of your app. It is used to reference the app from other config objects, + // and to generate the group used by your app in the app platform API. + appName: "quotas" + // groupOverride can be used to specify a non-appName-based API group. + // By default, an app's API group is LOWER(REPLACE(appName, '-', '')).ext.grafana.com, + // but there are cases where this needs to be changed. + // Keep in mind that changing this after an app is deployed can cause problems with clients and/or kind data. + groupOverride: "quotas.grafana.app" + + // versions is a map of versions supported by your app. Version names should follow the format "v" or + // "v(alpha|beta)". Each version contains the kinds your app manages for that version. + // If your app needs access to kinds managed by another app, use permissions.accessKinds to allow your app access. + versions: { + "v0alpha1": v0alpha1 + } + // extraPermissions contains any additional permissions your app may require to function. + // Your app will always have all permissions for each kind it manages (the items defined in 'kinds'). + extraPermissions: { + // If your app needs access to additional kinds supplied by other apps, you can list them here + accessKinds: [ + // Here is an example for your app accessing the playlist kind for reads and watch + // { + // group: "playlist.grafana.app" + // resource: "playlists" + // actions: ["get","list","watch"] + // } + ] + } +} + +// v1alpha1 is the v1alpha1 version of the app's API. +// It includes kinds which the v1alpha1 API serves, and (future) custom routes served globally from the v1alpha1 version. +v0alpha1: { + // kinds is the list of kinds served by this version + kinds: [] + // [OPTIONAL] + // served indicates whether this particular version is served by the API server. + // served should be set to false before a version is removed from the manifest entirely. + // served defaults to true if not present. + served: true + + routes: { + // namespaced contains namespace-scoped resource routes for the version, + // which are exposed as HTTP handlers on '/namespaces//'. + namespaced: { + "/usage": { + "GET": { + response: { + namespace: string + resource: string + group: string + usage: int64 + limit: int64 + } + request: { + query: { + group: string + resource: string + } + } + } + } + } + } + + // [OPTIONAL] + // Codegen is a trait that tells the grafana-app-sdk, or other code generation tooling, how to process this kind. + // If not present, default values within the codegen trait are used. + // If you wish to specify codegen per-version, put this section in the version's object + // (for example, v1alpha1) instead. + codegen: { + // [OPTIONAL] + // ts contains TypeScript code generation properties for the kind + ts: { + // [OPTIONAL] + // enabled indicates whether the CLI should generate front-end TypeScript code for the kind. + // Defaults to true if not present. + enabled: true + } + // [OPTIONAL] + // go contains go code generation properties for the kind + go: { + // [OPTIONAL] + // enabled indicates whether the CLI should generate back-end go code for the kind. + // Defaults to true if not present. + enabled: true + } + } +} diff --git a/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_object_gen.go b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_object_gen.go new file mode 100644 index 00000000000..b19b40d5e02 --- /dev/null +++ b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_object_gen.go @@ -0,0 +1,33 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type GetUsageRequestParamsObject struct { + metav1.TypeMeta `json:",inline"` + GetUsageRequestParams `json:",inline"` +} + +func NewGetUsageRequestParamsObject() *GetUsageRequestParamsObject { + return &GetUsageRequestParamsObject{} +} + +func (o *GetUsageRequestParamsObject) DeepCopyObject() runtime.Object { + dst := NewGetUsageRequestParamsObject() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetUsageRequestParamsObject) DeepCopyInto(dst *GetUsageRequestParamsObject) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + dstGetUsageRequestParams := GetUsageRequestParams{} + _ = resource.CopyObjectInto(&dstGetUsageRequestParams, &o.GetUsageRequestParams) +} + +var _ runtime.Object = NewGetUsageRequestParamsObject() diff --git a/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_types_gen.go b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_types_gen.go new file mode 100644 index 00000000000..45394a7f20f --- /dev/null +++ b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_request_params_types_gen.go @@ -0,0 +1,13 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +type GetUsageRequestParams struct { + Group string `json:"group"` + Resource string `json:"resource"` +} + +// NewGetUsageRequestParams creates a new GetUsageRequestParams object. +func NewGetUsageRequestParams() *GetUsageRequestParams { + return &GetUsageRequestParams{} +} diff --git a/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_body_types_gen.go b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_body_types_gen.go new file mode 100644 index 00000000000..eb87d022edd --- /dev/null +++ b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_body_types_gen.go @@ -0,0 +1,17 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type GetUsageBody struct { + Namespace string `json:"namespace"` + Resource string `json:"resource"` + Group string `json:"group"` + Usage int64 `json:"usage"` + Limit int64 `json:"limit"` +} + +// NewGetUsageBody creates a new GetUsageBody object. +func NewGetUsageBody() *GetUsageBody { + return &GetUsageBody{} +} diff --git a/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_object_types_gen.go b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_object_types_gen.go new file mode 100644 index 00000000000..87d6be2e587 --- /dev/null +++ b/apps/quotas/pkg/apis/quotas/v0alpha1/getusage_response_object_types_gen.go @@ -0,0 +1,37 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +k8s:openapi-gen=true +type GetUsage struct { + metav1.TypeMeta `json:",inline"` + GetUsageBody `json:",inline"` +} + +func NewGetUsage() *GetUsage { + return &GetUsage{} +} + +func (t *GetUsageBody) DeepCopyInto(dst *GetUsageBody) { + _ = resource.CopyObjectInto(dst, t) +} + +func (o *GetUsage) DeepCopyObject() runtime.Object { + dst := NewGetUsage() + o.DeepCopyInto(dst) + return dst +} + +func (o *GetUsage) DeepCopyInto(dst *GetUsage) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.GetUsageBody.DeepCopyInto(&dst.GetUsageBody) +} + +var _ runtime.Object = NewGetUsage() diff --git a/apps/quotas/pkg/apis/quotas_manifest.go b/apps/quotas/pkg/apis/quotas_manifest.go new file mode 100644 index 00000000000..e72524d59f3 --- /dev/null +++ b/apps/quotas/pkg/apis/quotas_manifest.go @@ -0,0 +1,213 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package apis + +import ( + "fmt" + "strings" + + "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" + "k8s.io/kube-openapi/pkg/validation/spec" + + v0alpha1 "github.com/grafana/grafana/apps/quotas/pkg/apis/quotas/v0alpha1" +) + +var appManifestData = app.ManifestData{ + AppName: "quotas", + Group: "quotas.grafana.app", + PreferredVersion: "v0alpha1", + Versions: []app.ManifestVersion{ + { + Name: "v0alpha1", + Served: true, + Kinds: []app.ManifestVersionKind{}, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{ + "/usage": { + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + + OperationId: "getUsage", + + Parameters: []*spec3.Parameter{ + + { + ParameterProps: spec3.ParameterProps{ + Name: "group", + In: "query", + Required: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + + { + ParameterProps: spec3.ParameterProps{ + Name: "resource", + In: "query", + Required: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + + Responses: &spec3.Responses{ + ResponsesProps: spec3.ResponsesProps{ + Default: &spec3.Response{ + ResponseProps: spec3.ResponseProps{ + Description: "Default OK response", + Content: map[string]*spec3.MediaType{ + "application/json": { + MediaTypeProps: spec3.MediaTypeProps{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + }, + }, + "group": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + }, + }, + "limit": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "resource": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "usage": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + }, + }, + }, + Required: []string{ + "namespace", + "resource", + "group", + "usage", + "limit", + "apiVersion", + "kind", + }, + }}, + }}, + }, + }, + }, + }}, + }, + }, + }, + }, + Cluster: map[string]spec3.PathProps{}, + Schemas: map[string]spec.Schema{}, + }, + }, + }, +} + +func LocalManifest() app.Manifest { + return app.NewEmbeddedManifest(appManifestData) +} + +func RemoteManifest() app.Manifest { + return app.NewAPIServerManifest("quotas") +} + +var kindVersionToGoType = map[string]resource.Kind{} + +// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. +// If there is no association for the provided Kind and Version, exists will return false. +func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { + goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] + return goType, exists +} + +var customRouteToGoResponseType = map[string]any{ + "v0alpha1||/usage|GET": v0alpha1.GetUsage{}, +} + +// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. +// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. +// If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) +func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoParamsType = map[string]runtime.Object{} + +func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +type GoTypeAssociator struct{} + +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + +func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { + return ManifestGoTypeAssociator(kind, version) +} +func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteResponsesAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { + return ManifestCustomRouteQueryAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/quotas/pkg/app/app.go b/apps/quotas/pkg/app/app.go new file mode 100644 index 00000000000..d4862661b60 --- /dev/null +++ b/apps/quotas/pkg/app/app.go @@ -0,0 +1,123 @@ +package app + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/logging" + "github.com/grafana/grafana-app-sdk/operator" + "github.com/grafana/grafana-app-sdk/resource" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + + unifiedStorage "github.com/grafana/grafana/pkg/storage/unified/resource" + + "github.com/grafana/grafana-app-sdk/simple" + quotasv0alpha1 "github.com/grafana/grafana/apps/quotas/pkg/apis/quotas/v0alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type QuotasAppConfig struct { + ResourceClient unifiedStorage.ResourceClient +} + +type QuotasHandler struct { + ResourceClient unifiedStorage.ResourceClient +} + +func NewQuotasHandler(cfg *QuotasAppConfig) *QuotasHandler { + return &QuotasHandler{ + ResourceClient: cfg.ResourceClient, + } +} + +// GetQuota handles requests for the GET /usage resource route +func (h *QuotasHandler) GetQuota(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error { + if !request.URL.Query().Has("group") { + // TODO its returning a 500 instead of 400 bad request + writer.WriteHeader(http.StatusBadRequest) + return fmt.Errorf("missing required query parameters: group") + } + if !request.URL.Query().Has("resource") { + writer.WriteHeader(http.StatusBadRequest) + return fmt.Errorf("missing required query parameters: resource") + } + group := request.URL.Query().Get("group") + res := request.URL.Query().Get("resource") + + quotaReq := &resourcepb.QuotaUsageRequest{ + Key: &resourcepb.ResourceKey{ + Namespace: request.ResourceIdentifier.Namespace, + Group: group, + Resource: res, + }, + } + quota, err := h.ResourceClient.GetQuotaUsage(ctx, quotaReq) + if err != nil { + return err + } + + writer.Header().Set("Content-Type", "application/json") + return json.NewEncoder(writer).Encode(quotasv0alpha1.GetUsage{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "quotas.grafana.com/v0alpha1", + Kind: "Quotas", + }, + GetUsageBody: quotasv0alpha1.GetUsageBody{ + Namespace: request.ResourceIdentifier.Namespace, + Group: group, + Resource: res, + Usage: quota.Usage, + Limit: quota.Limit, + }, + }) +} + +func New(cfg app.Config) (app.App, error) { + appConfig, ok := cfg.SpecificConfig.(*QuotasAppConfig) + if !ok { + return nil, fmt.Errorf("expected QuotasAppConfig but got %T", cfg.SpecificConfig) + } + handler := NewQuotasHandler(appConfig) + + simpleConfig := simple.AppConfig{ + Name: "quotas", + KubeConfig: cfg.KubeConfig, + InformerConfig: simple.AppInformerConfig{ + InformerOptions: operator.InformerOptions{ + ErrorHandler: func(ctx context.Context, err error) { + logging.FromContext(ctx).Error("Informer processing error", "error", err) + }, + }, + }, + ManagedKinds: []simple.AppManagedKind{}, + VersionedCustomRoutes: map[string]simple.AppVersionRouteHandlers{ + "v0alpha1": { + { + Namespaced: true, + Path: "usage", + Method: "GET", + }: handler.GetQuota, + }, + }, + } + + a, err := simple.NewApp(simpleConfig) + if err != nil { + return nil, err + } + + err = a.ValidateManifest(cfg.ManifestData) + if err != nil { + return nil, err + } + + return a, nil +} + +func GetKinds() map[schema.GroupVersion][]resource.Kind { + return map[schema.GroupVersion][]resource.Kind{} +} diff --git a/apps/quotas/pkg/app/app_test.go b/apps/quotas/pkg/app/app_test.go new file mode 100644 index 00000000000..252c2c4e240 --- /dev/null +++ b/apps/quotas/pkg/app/app_test.go @@ -0,0 +1,71 @@ +package app + +import ( + "context" + "net/http/httptest" + "net/url" + "testing" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestGetQuota(t *testing.T) { + t.Run("will return error when resource param is missing", func(t *testing.T) { + clientMock := resource.NewMockResourceClient(t) + handler := NewQuotasHandler(&QuotasAppConfig{ + ResourceClient: clientMock, + }) + url, err := url.Parse("http://localhost:3000/apis/quotas.grafana.app/v0alpha1/namespaces/stacks-1/usage?group=dashboard.grafana.app") + require.NoError(t, err) + req := &app.CustomRouteRequest{ + URL: url, + Method: "GET", + } + recorder := &httptest.ResponseRecorder{} + err = handler.GetQuota(context.Background(), recorder, req) + require.Error(t, err) + }) + + t.Run("will return error when group param is missing", func(t *testing.T) { + clientMock := resource.NewMockResourceClient(t) + handler := NewQuotasHandler(&QuotasAppConfig{ + ResourceClient: clientMock, + }) + url, err := url.Parse("http://localhost:3000/apis/quotas.grafana.app/v0alpha1/namespaces/stacks-1/usage?resource=dashboards") + require.NoError(t, err) + req := &app.CustomRouteRequest{ + URL: url, + Method: "GET", + } + recorder := &httptest.ResponseRecorder{} + err = handler.GetQuota(context.Background(), recorder, req) + require.Error(t, err) + }) + + t.Run("will return quotas response when params are valid", func(t *testing.T) { + clientMock := resource.NewMockResourceClient(t) + clientMock.On("GetQuotaUsage", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.QuotaUsageResponse{ + Error: nil, + Usage: 1, + Limit: 2, + }, nil) + handler := NewQuotasHandler(&QuotasAppConfig{ + ResourceClient: clientMock, + }) + url, err := url.Parse("http://localhost:3000/apis/quotas.grafana.app/v0alpha1/namespaces/stacks-1/usage?group=dashboard.grafana.app&resource=dashboards") + require.NoError(t, err) + req := &app.CustomRouteRequest{ + URL: url, + Method: "GET", + } + recorder := &httptest.ResponseRecorder{} + err = handler.GetQuota(context.Background(), recorder, req) + require.NoError(t, err) + + require.Equal(t, 200, recorder.Code) + }) +} diff --git a/apps/quotas/plugin/src/generated/quota/v0alpha1/quota_object_gen.ts b/apps/quotas/plugin/src/generated/quota/v0alpha1/quota_object_gen.ts new file mode 100644 index 00000000000..70f306a1b08 --- /dev/null +++ b/apps/quotas/plugin/src/generated/quota/v0alpha1/quota_object_gen.ts @@ -0,0 +1,49 @@ +/* + * 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; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface Quota { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/apps/quotas/plugin/src/generated/quota/v0alpha1/types.metadata.gen.ts b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/quotas/plugin/src/generated/quota/v0alpha1/types.spec.gen.ts b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..9209753fa64 --- /dev/null +++ b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.spec.gen.ts @@ -0,0 +1,14 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface Spec { + count: string; + limit: string; + kind: string; +} + +export const defaultSpec = (): Spec => ({ + count: "", + limit: "", + kind: "", +}); + diff --git a/apps/quotas/plugin/src/generated/quota/v0alpha1/types.status.gen.ts b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.status.gen.ts new file mode 100644 index 00000000000..01be8df7961 --- /dev/null +++ b/apps/quotas/plugin/src/generated/quota/v0alpha1/types.status.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface OperatorState { + // lastEvaluation is the ResourceVersion last evaluated + lastEvaluation: string; + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + state: "success" | "in_progress" | "failed"; + // descriptiveState is an optional more descriptive state field which has no requirements on format + descriptiveState?: string; + // details contains any extra information that is operator-specific + details?: Record; +} + +export const defaultOperatorState = (): OperatorState => ({ + lastEvaluation: "", + state: "success", +}); + +export interface Status { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + operatorStates?: Record; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ +}); + diff --git a/conf/defaults.ini b/conf/defaults.ini index a72b0183290..c2d7e4da3b6 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1747,9 +1747,8 @@ enabled = true #################################### Short Links ############################# [short_links] -# Short links that are never accessed will be deleted as cleanup. Time is set up in days. The default is 7 days. Maximum value is 365. -# 0 means they will be deleted approximately every 10 minutes. A negative value (such as -1) will disable expiration. -expire_time = 7 +# Short links that are never accessed will be deleted as cleanup. Time is set up in days. The default is -1 (never expire). Maximum value is 365. +expire_time = -1 #################################### Internal Grafana Metrics ############ # Metrics available at HTTP URL /metrics and /metrics/plugins/:pluginId diff --git a/conf/sample.ini b/conf/sample.ini index d6397f894e4..d1d50f0a72a 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -1689,8 +1689,8 @@ default_datasource_uid = #################################### Short Links ############################# [short_links] -# Short links which are never accessed will be deleted as cleanup. Time is in days. Default is 7 days. Max is 365. 0 means they will be deleted approximately every 10 minutes. -;expire_time = 7 +# Short links that are never accessed will be deleted as cleanup. Time is set up in days. The default is -1 (never expire). Maximum value is 365. +;expire_time = -1 #################################### Internal Grafana Metrics ########################## # Metrics available at HTTP URL /metrics and /metrics/plugins/:pluginId diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json index 9cb204da48a..f9ee5a8c4e3 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_new.json @@ -76,9 +76,9 @@ "barGlow": false, "centerGlow": false, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -153,9 +153,9 @@ "barGlow": false, "centerGlow": true, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -230,9 +230,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -307,9 +307,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -384,9 +384,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -461,9 +461,9 @@ "barGlow": true, "centerGlow": true, "rounded": false, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -538,9 +538,9 @@ "barGlow": true, "centerGlow": true, "rounded": false, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -628,9 +628,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -705,9 +705,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -782,9 +782,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -859,9 +859,9 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -953,9 +953,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1030,9 +1030,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1107,9 +1107,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1184,9 +1184,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1261,9 +1261,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false }, - "gradient": "none", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1355,9 +1355,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1436,9 +1436,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1517,9 +1517,9 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": true }, - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1607,10 +1607,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1691,10 +1691,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1789,10 +1789,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1874,11 +1874,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "sparkline": false, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "scheme", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -1959,10 +1958,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], @@ -2043,10 +2042,10 @@ "barGlow": true, "centerGlow": true, "rounded": true, - "spotlight": true + "spotlight": true, + "gradient": true }, "glow": "both", - "gradient": "hue", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], diff --git a/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json b/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json index 61bec02a59b..b071ddff802 100644 --- a/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json +++ b/devenv/dev-dashboards/panel-gauge/gauge_tests_old_to_new.json @@ -947,7 +947,6 @@ }, "orientation": "auto", "shape": "gauge", - "gradient": "none", "barWidthFactor": 0.5, "segmentCount": 70, "segmentSpacing": 0.3, @@ -958,7 +957,8 @@ "barGlow": false, "centerGlow": false, "rounded": false, - "spotlight": false + "spotlight": false, + "gradient": false } } }, diff --git a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md index 641d9bb4180..5fb3705232e 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md @@ -238,6 +238,7 @@ For more information on Cloud Access Policies and how to use them, see [Access p | `alert.notifications.templates:read` | None | Read templates. | | `alert.notifications.templates:write` | None | Create new or update existing templates. | | `alert.notifications.templates:delete` | None | Delete existing templates. | +| `alert.notifications.templates.test:write` | None | Test templates with custom payloads (preview and payload editor functionality). | | `alert.notifications.routes:read` | None | Read notification policies. | | `alert.notifications.routes:write` | None | Create new, update or delete notification policies | diff --git a/docs/sources/alerting/set-up/configure-rbac/_index.md b/docs/sources/alerting/set-up/configure-rbac/_index.md index 33fb9e57dcb..9c591e7361d 100644 --- a/docs/sources/alerting/set-up/configure-rbac/_index.md +++ b/docs/sources/alerting/set-up/configure-rbac/_index.md @@ -64,6 +64,7 @@ Grafana Alerting has the following permissions. | `alert.notifications.templates:read` | n/a | Read templates. | | `alert.notifications.templates:write` | n/a | Create new or update existing templates. | | `alert.notifications.templates:delete` | n/a | Delete existing templates. | +| `alert.notifications.templates.test:write` | n/a | Test templates with custom payloads (preview and payload editor functionality). | | `alert.notifications.routes:read` | n/a | Read notification policies. | | `alert.notifications.routes:write` | n/a | Create new, update and update notification policies. | diff --git a/docs/sources/alerting/set-up/configure-rbac/access-roles/index.md b/docs/sources/alerting/set-up/configure-rbac/access-roles/index.md index 668e84c41cb..be1489eb1c4 100644 --- a/docs/sources/alerting/set-up/configure-rbac/access-roles/index.md +++ b/docs/sources/alerting/set-up/configure-rbac/access-roles/index.md @@ -60,7 +60,7 @@ Details of the fixed roles and the access they provide for Grafana Alerting are | Contact Point Creator: `fixed:alerting.receivers:creator` | `alert.notifications.receivers:create` | Create a new contact point. The user is automatically granted full access to the created contact point. | | Contact Point Writer: `fixed:alerting.receivers:writer` | `alert.notifications.receivers:read`, `alert.notifications.receivers:write`, `alert.notifications.receivers:delete` for scope `receivers:*` and
`alert.notifications.receivers:create` | Create a new contact point and manage all existing contact points. | | Templates Reader: `fixed:alerting.templates:reader` | `alert.notifications.templates:read` | Read all notification templates. | -| Templates Writer: `fixed:alerting.templates:writer` | `alert.notifications.templates:read`, `alert.notifications.templates:write`, `alert.notifications.templates:delete` | Create new and manage existing notification templates. | +| Templates Writer: `fixed:alerting.templates:writer` | `alert.notifications.templates:read`, `alert.notifications.templates:write`, `alert.notifications.templates:delete`, `alert.notifications.templates.test:write` | Create new and manage existing notification templates. Test templates with custom payloads. | | Time Intervals Reader: `fixed:alerting.time-intervals:reader` | `alert.notifications.time-intervals:read` | Read all time intervals. | | Time Intervals Writer: `fixed:alerting.time-intervals:writer` | `alert.notifications.time-intervals:read`, `alert.notifications.time-intervals:write`, `alert.notifications.time-intervals:delete` | Create new and manage existing time intervals. | | Notification Policies Reader: `fixed:alerting.routes:reader` | `alert.notifications.routes:read` | Read all time intervals. | diff --git a/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md b/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md index 6b145841bbf..d43e0ec2978 100644 --- a/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md +++ b/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md @@ -174,6 +174,8 @@ You can also override this setting in a dashboard panel under its data source op Frozen indices are [deprecated in Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/frozen-indices.html) since v7.14. {{< /admonition >}} +- **Default query mode** - Specifies which query mode the data source uses by default. Options are `Metrics`, `Logs`, `Raw data`, and `Raw document`. The default is `Metrics`. + ### Logs In this section you can configure which fields the data source uses for log messages and log levels. diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 13c2b77fd2f..1a3e4aea652 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -2142,17 +2142,13 @@ Configures settings around the short link feature. #### `expire_time` -Short links that are never accessed are considered expired or stale and are deleted as cleanup. +Short links that are never accessed are considered expired or stale and can be deleted as cleanup. Set the expiration time in days. -The default is `7` days. +The default is `-1` days (never expire). The maximum is `365` days. -A setting above the maximum uses the value `365` instead. -Setting `0` means the short links are cleaned up approximately every 10 minutes. -A negative value such as `-1` disables expiry. -{{< admonition type="caution" >}} -Short links without an expiration increase the size of the database and can't be deleted. Grafana recommends setting a duration based on your specific use case -{{< /admonition >}} +A setting above the maximum uses the value `365` instead. +A negative value such as `-1` disables expiry.
diff --git a/docs/sources/upgrade-guide/upgrade-v12.0/index.md b/docs/sources/upgrade-guide/upgrade-v12.0/index.md index 779950c0fd4..feb1061dceb 100644 --- a/docs/sources/upgrade-guide/upgrade-v12.0/index.md +++ b/docs/sources/upgrade-guide/upgrade-v12.0/index.md @@ -80,3 +80,53 @@ Since Grafana 10.2, the endpoint to check compatible versions when installing a #### What if I want to ignore the compatibility check? We _do not_ recommend installing plugins declared as incompatible. However, if you need to force install a plugin despite it being declared as incompatible, refer to the [Installing a plugin from a ZIP](https://grafana.com/docs/grafana/latest/administration/plugin-management/#install-a-plugin-from-a-zip-file) guidance. + +### PostgreSQL annotation table migration + +**Plan for increased disk usage when upgrading from Grafana v11.x** + +Upgrading from Grafana v11.x to Grafana v12.x triggers a full-table rewrite of the PostgreSQL `annotation` table. The migration populates the new `dashboard_uid` column, which causes PostgreSQL to rewrite the entire table and rebuild its indexes. + +Environments with large annotation datasets can experience significant temporary disk usage increase, which may lead to: + +- Rapid disk consumption on the PostgreSQL data volume +- Database migration failures (for example, "could not extend file: No space left on device") +- Grafana startup failures +- Extended downtime during the upgrade process + +#### How do I know if I'm affected? + +You're affected if you're upgrading from Grafana v11.x to v12.x and you have a large `annotation` table in your PostgreSQL database. + +To check your annotation table size, connect to your PostgreSQL database and run the following query: + +```sql +SELECT + pg_size_pretty(pg_relation_size('annotation')) AS table_size, + pg_size_pretty(pg_indexes_size('annotation')) AS indexes_size, + pg_size_pretty(pg_total_relation_size('annotation')) AS total_size; +``` + +If your total size is several gigabytes or more, you should plan accordingly before upgrading. + +#### What should I do before upgrading? + +Before you upgrade, take the following steps: + +1. **Verify available disk space**: Ensure you have at least 2-3 times the current `annotation` table size available as free disk space on your PostgreSQL data volume. + +2. **Review your annotation data**: Consider whether you need to retain all historical annotations. + +3. **Clean up old annotations (optional)**: If you have annotations you don't need, remove them before upgrading. + +4. **Back up your database**: Always back up your Grafana database before performing an upgrade. For more information, refer to [Back up Grafana](#back-up-grafana). + +#### What should I do after upgrading? + +After successfully upgrading to Grafana v12.x, you can reclaim disk space by running a `VACUUM FULL` operation on the `annotation` table during a maintenance window: + +```sql +VACUUM FULL annotation; +``` + +This operation requires a lock on the table and may take significant time depending on the table size. Plan to run this during a low-traffic period. diff --git a/docs/sources/upgrade-guide/upgrade-v12.1/index.md b/docs/sources/upgrade-guide/upgrade-v12.1/index.md index 74095298c90..7dbcb11ece9 100644 --- a/docs/sources/upgrade-guide/upgrade-v12.1/index.md +++ b/docs/sources/upgrade-guide/upgrade-v12.1/index.md @@ -20,3 +20,53 @@ weight: 499 {{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} ## Technical notes + +### PostgreSQL annotation table migration + +**Plan for increased disk usage when upgrading from Grafana v11.x** + +Upgrading from Grafana v11.x to Grafana v12.x triggers a full-table rewrite of the PostgreSQL `annotation` table. The migration populates the new `dashboard_uid` column, which causes PostgreSQL to rewrite the entire table and rebuild its indexes. + +Environments with large annotation datasets can experience significant temporary disk usage increase, which may lead to: + +- Rapid disk consumption on the PostgreSQL data volume +- Database migration failures (for example, "could not extend file: No space left on device") +- Grafana startup failures +- Extended downtime during the upgrade process + +#### How do I know if I'm affected? + +You're affected if you're upgrading from Grafana v11.x to v12.x and you have a large `annotation` table in your PostgreSQL database. + +To check your annotation table size, connect to your PostgreSQL database and run the following query: + +```sql +SELECT + pg_size_pretty(pg_relation_size('annotation')) AS table_size, + pg_size_pretty(pg_indexes_size('annotation')) AS indexes_size, + pg_size_pretty(pg_total_relation_size('annotation')) AS total_size; +``` + +If your total size is several gigabytes or more, you should plan accordingly before upgrading. + +#### What should I do before upgrading? + +Before you upgrade, take the following steps: + +1. **Verify available disk space**: Ensure you have at least 2-3 times the current `annotation` table size available as free disk space on your PostgreSQL data volume. + +2. **Review your annotation data**: Consider whether you need to retain all historical annotations. + +3. **Clean up old annotations (optional)**: If you have annotations you don't need, remove them before upgrading. + +4. **Back up your database**: Always back up your Grafana database before performing an upgrade. For more information, refer to [Back up Grafana](#back-up-grafana). + +#### What should I do after upgrading? + +After successfully upgrading to Grafana v12.x, you can reclaim disk space by running a `VACUUM FULL` operation on the `annotation` table during a maintenance window: + +```sql +VACUUM FULL annotation; +``` + +This operation requires a lock on the table and may take significant time depending on the table size. Plan to run this during a low-traffic period. diff --git a/docs/sources/upgrade-guide/upgrade-v12.2/index.md b/docs/sources/upgrade-guide/upgrade-v12.2/index.md index d2605fc51d1..1e900d65793 100644 --- a/docs/sources/upgrade-guide/upgrade-v12.2/index.md +++ b/docs/sources/upgrade-guide/upgrade-v12.2/index.md @@ -18,3 +18,55 @@ weight: 498 {{< docs/shared lookup="back-up/back-up-grafana.md" source="grafana" version="" leveloffset="+1" >}} {{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} + +## Technical notes + +### PostgreSQL annotation table migration + +**Plan for increased disk usage when upgrading from Grafana v11.x** + +Upgrading from Grafana v11.x to Grafana v12.x triggers a full-table rewrite of the PostgreSQL `annotation` table. The migration populates the new `dashboard_uid` column, which causes PostgreSQL to rewrite the entire table and rebuild its indexes. + +Environments with large annotation datasets can experience significant temporary disk usage increase, which may lead to: + +- Rapid disk consumption on the PostgreSQL data volume +- Database migration failures (for example, "could not extend file: No space left on device") +- Grafana startup failures +- Extended downtime during the upgrade process + +#### How do I know if I'm affected? + +You're affected if you're upgrading from Grafana v11.x to v12.x and you have a large `annotation` table in your PostgreSQL database. + +To check your annotation table size, connect to your PostgreSQL database and run the following query: + +```sql +SELECT + pg_size_pretty(pg_relation_size('annotation')) AS table_size, + pg_size_pretty(pg_indexes_size('annotation')) AS indexes_size, + pg_size_pretty(pg_total_relation_size('annotation')) AS total_size; +``` + +If your total size is several gigabytes or more, you should plan accordingly before upgrading. + +#### What should I do before upgrading? + +Before you upgrade, take the following steps: + +1. **Verify available disk space**: Ensure you have at least 2-3 times the current `annotation` table size available as free disk space on your PostgreSQL data volume. + +2. **Review your annotation data**: Consider whether you need to retain all historical annotations. + +3. **Clean up old annotations (optional)**: If you have annotations you don't need, remove them before upgrading. + +4. **Back up your database**: Always back up your Grafana database before performing an upgrade. For more information, refer to [Back up Grafana](#back-up-grafana). + +#### What should I do after upgrading? + +After successfully upgrading to Grafana v12.x, you can reclaim disk space by running a `VACUUM FULL` operation on the `annotation` table during a maintenance window: + +```sql +VACUUM FULL annotation; +``` + +This operation requires a lock on the table and may take significant time depending on the table size. Plan to run this during a low-traffic period. diff --git a/docs/sources/upgrade-guide/upgrade-v12.3/index.md b/docs/sources/upgrade-guide/upgrade-v12.3/index.md index 8d3dacbf396..dc8b1b8e398 100644 --- a/docs/sources/upgrade-guide/upgrade-v12.3/index.md +++ b/docs/sources/upgrade-guide/upgrade-v12.3/index.md @@ -18,3 +18,55 @@ weight: 497 {{< docs/shared lookup="back-up/back-up-grafana.md" source="grafana" version="" leveloffset="+1" >}} {{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} + +## Technical notes + +### PostgreSQL annotation table migration + +**Plan for increased disk usage when upgrading from Grafana v11.x** + +Upgrading from Grafana v11.x to Grafana v12.x triggers a full-table rewrite of the PostgreSQL `annotation` table. The migration populates the new `dashboard_uid` column, which causes PostgreSQL to rewrite the entire table and rebuild its indexes. + +Environments with large annotation datasets can experience significant temporary disk usage increase, which may lead to: + +- Rapid disk consumption on the PostgreSQL data volume +- Database migration failures (for example, "could not extend file: No space left on device") +- Grafana startup failures +- Extended downtime during the upgrade process + +#### How do I know if I'm affected? + +You're affected if you're upgrading from Grafana v11.x to v12.x and you have a large `annotation` table in your PostgreSQL database. + +To check your annotation table size, connect to your PostgreSQL database and run the following query: + +```sql +SELECT + pg_size_pretty(pg_relation_size('annotation')) AS table_size, + pg_size_pretty(pg_indexes_size('annotation')) AS indexes_size, + pg_size_pretty(pg_total_relation_size('annotation')) AS total_size; +``` + +If your total size is several gigabytes or more, you should plan accordingly before upgrading. + +#### What should I do before upgrading? + +Before you upgrade, take the following steps: + +1. **Verify available disk space**: Ensure you have at least 2-3 times the current `annotation` table size available as free disk space on your PostgreSQL data volume. + +2. **Review your annotation data**: Consider whether you need to retain all historical annotations. + +3. **Clean up old annotations (optional)**: If you have annotations you don't need, remove them before upgrading. + +4. **Back up your database**: Always back up your Grafana database before performing an upgrade. For more information, refer to [Back up Grafana](#back-up-grafana). + +#### What should I do after upgrading? + +After successfully upgrading to Grafana v12.x, you can reclaim disk space by running a `VACUUM FULL` operation on the `annotation` table during a maintenance window: + +```sql +VACUUM FULL annotation; +``` + +This operation requires a lock on the table and may take significant time depending on the table size. Plan to run this during a low-traffic period. diff --git a/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md b/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md index 69dbf4a2547..aef32f9b569 100644 --- a/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md +++ b/docs/sources/visualizations/dashboards/variables/add-template-variables/index.md @@ -171,6 +171,7 @@ Query expressions are different for each data source. For more information, refe - If you need more room in a single input field query editor, then hover your cursor over the lines in the lower right corner of the field and drag downward to expand. 1. (Optional) In the **Regex** field, type a regular expression to filter or capture specific parts of the names returned by your data source query. To see examples, refer to [Filter variables with a regular expression](#filter-variables-with-regex). +1. Under **Apply regex to**, select **Variable value** or **Display text** to choose where the regex pattern is applied. The default is **Variable value**. 1. In the **Sort** drop-down list, select the sort order for values to be displayed in the dropdown list. The default option, **Disabled**, means that the order of options returned by your data source query is used. 1. Under **Refresh**, select when the variable should update options: - **On dashboard load** - Queries the data source every time the dashboard loads. This slows down dashboard loading, because the variable query needs to be completed before dashboard can be initialized. diff --git a/docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md b/docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md index b6c9471e3e3..5b121028404 100644 --- a/docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md +++ b/docs/sources/visualizations/panels-visualizations/query-transform-data/_index.md @@ -162,6 +162,12 @@ To access your saved queries, click **+ Add from saved queries** or **Replace wi Clicking **+ Add from saved queries** adds an additional query, while clicking **Replace with saved query** updates your existing query. +{{< admonition type="note" >}} +Users with Admin and Editor roles can create and save queries for reuse. +While Admin users can edit or delete any saved queries, users with the Editor role can only edit or delete the queries they've saved. +Viewers can only reuse queries. +{{< /admonition >}} + #### Save a query To save a query you've created: diff --git a/e2e-playwright/dashboards-suite/new-query-variable.spec.ts b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts index ed92c79ee36..852261dbaf5 100644 --- a/e2e-playwright/dashboards-suite/new-query-variable.spec.ts +++ b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts @@ -79,6 +79,16 @@ test.describe( await expect(regexInput).toHaveAttribute('placeholder', '/.*-(?.*)-(?.*)-.*/'); await expect(regexInput).toHaveValue(''); + // Check regex apply to field - should default to "Variable value" + const regexApplyToField = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2 + ); + await expect(regexApplyToField).toBeVisible(); + const variableValueRadio = page.getByRole('radio', { name: 'Variable value' }); + await expect(variableValueRadio).toBeChecked(); + const displayTextRadio = page.getByRole('radio', { name: 'Display text' }); + await expect(displayTextRadio).not.toBeChecked(); + const sortSelect = dashboardPage.getByGrafanaSelector( selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2 ); diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 9ecdfaed054..bc54f1c116e 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -397,7 +397,7 @@ }, "packages/grafana-prometheus/src/language_provider.ts": { "@typescript-eslint/no-explicit-any": { - "count": 7 + "count": 2 } }, "packages/grafana-prometheus/src/language_utils.ts": { @@ -669,7 +669,7 @@ }, "packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx": { "no-restricted-syntax": { - "count": 3 + "count": 2 } }, "packages/grafana-ui/src/components/JSONFormatter/json_explorer/json_explorer.ts": { @@ -786,11 +786,6 @@ "count": 13 } }, - "packages/grafana-ui/src/components/Sparkline/Sparkline.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "packages/grafana-ui/src/components/Table/Cells/TableCell.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 3 @@ -886,11 +881,6 @@ "count": 1 } }, - "packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "packages/grafana-ui/src/components/VizLegend/types.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 @@ -2001,11 +1991,6 @@ "count": 1 } }, - "public/app/features/dashboard-scene/settings/variables/components/VariableTextAreaField.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "public/app/features/dashboard-scene/settings/variables/components/VariableTextField.tsx": { "no-restricted-syntax": { "count": 1 @@ -2062,9 +2047,6 @@ }, "@typescript-eslint/no-explicit-any": { "count": 3 - }, - "no-restricted-syntax": { - "count": 3 } }, "public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx": { diff --git a/go.mod b/go.mod index e589e5a18a5..40e3288158d 100644 --- a/go.mod +++ b/go.mod @@ -250,6 +250,7 @@ require ( github.com/grafana/grafana/apps/plugins v0.0.0 // @grafana/plugins-platform-backend github.com/grafana/grafana/apps/preferences v0.0.0 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/apps/provisioning v0.0.0 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 // @grafana/grafana-search-and-storage github.com/grafana/grafana/apps/scope v0.0.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana/apps/secret v0.0.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana/apps/shorturl v0.0.0 // @grafana/sharing-squad @@ -665,7 +666,7 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect - github.com/ebitengine/purego v0.8.4 // indirect + github.com/ebitengine/purego v0.8.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/swag/conv v0.25.1 // indirect github.com/go-openapi/swag/fileutils v0.25.1 // indirect @@ -686,7 +687,7 @@ require ( github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/shirou/gopsutil/v4 v4.25.3 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.8.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect diff --git a/go.sum b/go.sum index 91104bfeaf4..f4f91335d2b 100644 --- a/go.sum +++ b/go.sum @@ -1142,8 +1142,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= @@ -1653,6 +1653,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k= +github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDPMdshj3QMvpXn+wK4T8awF9n2sd8i4YRiGqX2xTvg= +github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s= @@ -2393,8 +2395,8 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3 h1:/4/IJi5iyTdh6mqOUaASW148HQpujYiHl0Wl78dSOSc= github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3/go.mod h1:aJIMhRsunltJR926EB2MUg8qHemFQDreSB33pyto2Ps= -github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= -github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE= +github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= diff --git a/go.work b/go.work index ecfab4e96ce..884f393673e 100644 --- a/go.work +++ b/go.work @@ -23,6 +23,7 @@ use ( ./apps/plugins ./apps/preferences ./apps/provisioning + ./apps/quotas ./apps/scope ./apps/secret ./apps/shorturl diff --git a/go.work.sum b/go.work.sum index 3017d8eb878..72f38753b02 100644 --- a/go.work.sum +++ b/go.work.sum @@ -680,8 +680,6 @@ github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6 github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= -github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= -github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/efficientgo/tools/core v0.0.0-20220225185207-fe763185946b h1:ZHiD4/yE4idlbqvAO6iYCOYRzOMRpxkW+FKasRA3tsQ= github.com/efficientgo/tools/core v0.0.0-20220225185207-fe763185946b/go.mod h1:OmVcnJopJL8d3X3sSXTiypGoUSgFq1aDGmlrdi9dn/M= github.com/elastic/elastic-transport-go/v8 v8.6.1 h1:h2jQRqH6eLGiBSN4eZbQnJLtL4bC5b4lfVFRjw2R4e4= @@ -873,7 +871,6 @@ github.com/grafana/grafana-app-sdk v0.41.0 h1:SYHN3U7B1myRKY3UZZDkFsue9TDmAOap0U github.com/grafana/grafana-app-sdk v0.41.0/go.mod h1:Wg/3vEZfok1hhIWiHaaJm+FwkosfO98o8KbeLFEnZpY= github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= github.com/grafana/grafana-app-sdk/logging v0.38.0/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= github.com/grafana/grafana-app-sdk/logging v0.39.0 h1:3GgN5+dUZYqq74Q+GT9/ET+yo+V54zWQk/Q2/JsJQB4= github.com/grafana/grafana-app-sdk/logging v0.39.0/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= @@ -887,9 +884,10 @@ github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5 github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-app-sdk/logging v0.48.0 h1:xolkQxBlA2LQF4hprKIAeu+zUem1DigYZ6XC1TOhFJE= github.com/grafana/grafana-app-sdk/logging v0.48.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-app-sdk/logging v0.48.2 h1:tI+a9slUvxKUgweXDzUqkca2LWV3g1UdaSvwt8nQNHg= github.com/grafana/grafana-app-sdk/logging v0.48.2/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk/logging v0.48.5 h1:vWiTZrsSscbC5IQq2heWXm0dXhvg40nXIeUTCdE9qsc= +github.com/grafana/grafana-app-sdk/logging v0.48.5/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-app-sdk/plugin v0.41.0 h1:ShUvGpAVzM3UxcsfwS6l/lwW4ytDeTbCQXf8w2P8Yp8= github.com/grafana/grafana-app-sdk/plugin v0.41.0/go.mod h1:YIhimVfAqtOp3kdhxOanaSZjypVKh/bYxf9wfFfhDm0= github.com/grafana/grafana-aws-sdk v0.38.2 h1:TzQD0OpWsNjtldi5G5TLDlBRk8OyDf+B5ujcoAu4Dp0= @@ -926,6 +924,7 @@ github.com/grafana/grafana/apps/preferences v0.0.0-20250805120145-0c5a00302924 h github.com/grafana/grafana/apps/preferences v0.0.0-20250805120145-0c5a00302924/go.mod h1:NQlHMO5fHhjexw71wVjv522532NRvFg5F4tcjUEktjs= github.com/grafana/grafana/apps/preferences v0.0.0-20250805123034-066163d71001 h1:y2AHkdji2I+zXv8rsSC8OjWEzJJjqW5OlmCsZR5+RuU= github.com/grafana/grafana/apps/preferences v0.0.0-20250805123034-066163d71001/go.mod h1:NQlHMO5fHhjexw71wVjv522532NRvFg5F4tcjUEktjs= +github.com/grafana/grafana/apps/quotas v0.0.0-20251209171149-4b999cd94388/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d/go.mod h1:1sq0guad+G4SUTlBgx7SXfhnzy7D86K/LcVOtiQCiMA= github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d/go.mod h1:tfLnBpPYgwrBMRz4EXqPCZJyCjEG4Ev37FSlXnocJ2c= github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d/go.mod h1:CXpwZ3Mkw6xVlGKc0SqUxqXCP3Uv182q6qAQnLaLxRg= @@ -956,7 +955,6 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9K github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= @@ -1375,7 +1373,6 @@ github.com/richardartoul/molecule v1.0.0/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+u github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= @@ -1412,8 +1409,6 @@ github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKl github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= -github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE= -github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= @@ -1593,7 +1588,6 @@ go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5queth go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/collector v0.121.0/go.mod h1:M4TlnmkjIgishm2DNCk9K3hMKTmAsY9w8cNFsp9EchM= go.opentelemetry.io/collector v0.124.0/go.mod h1:QzERYfmHUedawjr8Ph/CBEEkVqWS8IlxRLAZt+KHlCg= go.opentelemetry.io/collector/client v1.29.0/go.mod h1:LCUoEV2KCTKA1i+/txZaGsSPVWUcqeOV6wCfNsAippE= @@ -1880,7 +1874,6 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8 go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= @@ -2081,9 +2074,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go. google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822 h1:zWFRixYR5QlotL+Uv3YfsPRENIrQFXiGs+iwqel6fOQ= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= @@ -2112,9 +2103,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= @@ -2135,7 +2124,6 @@ google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7E google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= @@ -2250,7 +2238,6 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ih sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/structured-merge-diff/v6 v6.2.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 346edbb4753..454d02f270f 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -218,6 +218,8 @@ lineage: schemas: [{ // Optional field, if you want to extract part of a series name or metric node segment. // Named capture groups can be used to separate the display text and value. regex?: string + // Determine whether regex applies to variable value or display text + regexApplyTo?: #VariableRegexApplyTo // Additional static options for query variable staticOptions?: [...#VariableOption] // Ordering of static options in relation to options returned from data source for query variable @@ -245,6 +247,10 @@ lineage: schemas: [{ // Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing), 3 (show under the controls dropdown menu). #VariableHide: 0 | 1 | 2 | 3 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable|inControlsMenu") @grafana(TSVeneer="type") + // Determine whether regex applies to variable value or display text + // Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users) + #VariableRegexApplyTo: "value" | "text" @cuetsy(kind="type") + // Sort variable options // Accepted values are: // `0`: No sorting diff --git a/package.json b/package.json index e9ce0640360..683ed2373de 100644 --- a/package.json +++ b/package.json @@ -296,8 +296,8 @@ "@grafana/plugin-ui": "^0.11.1", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "6.47.1", - "@grafana/scenes-react": "6.47.1", + "@grafana/scenes": "6.49.0", + "@grafana/scenes-react": "6.49.0", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/packages/grafana-api-clients/src/clients/rtkq/logsdrilldown/v1alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/logsdrilldown/v1alpha1/endpoints.gen.ts index 835194b59e5..22464a2b3fa 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/logsdrilldown/v1alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/logsdrilldown/v1alpha1/endpoints.gen.ts @@ -1,5 +1,10 @@ import { api } from './baseAPI'; -export const addTagTypes = ['API Discovery', 'LogsDrilldownDefaults', 'LogsDrilldown'] as const; +export const addTagTypes = [ + 'API Discovery', + 'LogsDrilldownDefaultColumns', + 'LogsDrilldownDefaults', + 'LogsDrilldown', +] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, @@ -10,6 +15,183 @@ const injectedRtkApi = api query: () => ({ url: `/` }), providesTags: ['API Discovery'], }), + listLogsDrilldownDefaultColumns: build.query< + ListLogsDrilldownDefaultColumnsApiResponse, + ListLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns`, + params: { + pretty: queryArg.pretty, + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['LogsDrilldownDefaultColumns'], + }), + createLogsDrilldownDefaultColumns: build.mutation< + CreateLogsDrilldownDefaultColumnsApiResponse, + CreateLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns`, + method: 'POST', + body: queryArg.logsDrilldownDefaultColumns, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['LogsDrilldownDefaultColumns'], + }), + deletecollectionLogsDrilldownDefaultColumns: build.mutation< + DeletecollectionLogsDrilldownDefaultColumnsApiResponse, + DeletecollectionLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['LogsDrilldownDefaultColumns'], + }), + getLogsDrilldownDefaultColumns: build.query< + GetLogsDrilldownDefaultColumnsApiResponse, + GetLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['LogsDrilldownDefaultColumns'], + }), + replaceLogsDrilldownDefaultColumns: build.mutation< + ReplaceLogsDrilldownDefaultColumnsApiResponse, + ReplaceLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${queryArg.name}`, + method: 'PUT', + body: queryArg.logsDrilldownDefaultColumns, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['LogsDrilldownDefaultColumns'], + }), + deleteLogsDrilldownDefaultColumns: build.mutation< + DeleteLogsDrilldownDefaultColumnsApiResponse, + DeleteLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['LogsDrilldownDefaultColumns'], + }), + updateLogsDrilldownDefaultColumns: build.mutation< + UpdateLogsDrilldownDefaultColumnsApiResponse, + UpdateLogsDrilldownDefaultColumnsApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['LogsDrilldownDefaultColumns'], + }), + getLogsDrilldownDefaultColumnsStatus: build.query< + GetLogsDrilldownDefaultColumnsStatusApiResponse, + GetLogsDrilldownDefaultColumnsStatusApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${queryArg.name}/status`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['LogsDrilldownDefaultColumns'], + }), + replaceLogsDrilldownDefaultColumnsStatus: build.mutation< + ReplaceLogsDrilldownDefaultColumnsStatusApiResponse, + ReplaceLogsDrilldownDefaultColumnsStatusApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${queryArg.name}/status`, + method: 'PUT', + body: queryArg.logsDrilldownDefaultColumns, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['LogsDrilldownDefaultColumns'], + }), + updateLogsDrilldownDefaultColumnsStatus: build.mutation< + UpdateLogsDrilldownDefaultColumnsStatusApiResponse, + UpdateLogsDrilldownDefaultColumnsStatusApiArg + >({ + query: (queryArg) => ({ + url: `/logsdrilldowndefaultcolumns/${queryArg.name}/status`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['LogsDrilldownDefaultColumns'], + }), listLogsDrilldownDefaults: build.query({ query: (queryArg) => ({ url: `/logsdrilldowndefaults`, @@ -340,6 +522,218 @@ const injectedRtkApi = api export { injectedRtkApi as generatedAPI }; export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList; export type GetApiResourcesApiArg = void; +export type ListLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ LogsDrilldownDefaultColumnsList; +export type ListLogsDrilldownDefaultColumnsApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type CreateLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ + | LogsDrilldownDefaultColumns + | /** status 201 Created */ LogsDrilldownDefaultColumns + | /** status 202 Accepted */ LogsDrilldownDefaultColumns; +export type CreateLogsDrilldownDefaultColumnsApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + logsDrilldownDefaultColumns: LogsDrilldownDefaultColumns; +}; +export type DeletecollectionLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ Status; +export type DeletecollectionLogsDrilldownDefaultColumnsApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; +export type GetLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ LogsDrilldownDefaultColumns; +export type GetLogsDrilldownDefaultColumnsApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ + | LogsDrilldownDefaultColumns + | /** status 201 Created */ LogsDrilldownDefaultColumns; +export type ReplaceLogsDrilldownDefaultColumnsApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + logsDrilldownDefaultColumns: LogsDrilldownDefaultColumns; +}; +export type DeleteLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ + | Status + | /** status 202 Accepted */ Status; +export type DeleteLogsDrilldownDefaultColumnsApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; +export type UpdateLogsDrilldownDefaultColumnsApiResponse = /** status 200 OK */ + | LogsDrilldownDefaultColumns + | /** status 201 Created */ LogsDrilldownDefaultColumns; +export type UpdateLogsDrilldownDefaultColumnsApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; +export type GetLogsDrilldownDefaultColumnsStatusApiResponse = /** status 200 OK */ LogsDrilldownDefaultColumns; +export type GetLogsDrilldownDefaultColumnsStatusApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceLogsDrilldownDefaultColumnsStatusApiResponse = /** status 200 OK */ + | LogsDrilldownDefaultColumns + | /** status 201 Created */ LogsDrilldownDefaultColumns; +export type ReplaceLogsDrilldownDefaultColumnsStatusApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + logsDrilldownDefaultColumns: LogsDrilldownDefaultColumns; +}; +export type UpdateLogsDrilldownDefaultColumnsStatusApiResponse = /** status 200 OK */ + | LogsDrilldownDefaultColumns + | /** status 201 Created */ LogsDrilldownDefaultColumns; +export type UpdateLogsDrilldownDefaultColumnsStatusApiArg = { + /** name of the LogsDrilldownDefaultColumns */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; export type ListLogsDrilldownDefaultsApiResponse = /** status 200 OK */ LogsDrilldownDefaultsList; export type ListLogsDrilldownDefaultsApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ @@ -874,13 +1268,21 @@ export type ObjectMeta = { Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ uid?: string; }; -export type LogsDrilldownDefaultsSpec = { - defaultFields: string[]; - interceptDismissed: boolean; - prettifyJSON: boolean; - wrapLogMessage: boolean; +export type LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel = { + key: string; + value: string; }; -export type LogsDrilldownDefaultsOperatorState = { +export type LogsDrilldownDefaultColumnsLogsDefaultColumnsLabels = LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel[]; +export type LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord = { + columns: string[]; + labels: LogsDrilldownDefaultColumnsLogsDefaultColumnsLabels; +}; +export type LogsDrilldownDefaultColumnsLogsDefaultColumnsRecords = + LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord[]; +export type LogsDrilldownDefaultColumnsSpec = { + records: LogsDrilldownDefaultColumnsLogsDefaultColumnsRecords; +}; +export type LogsDrilldownDefaultColumnsOperatorState = { /** 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 */ @@ -895,7 +1297,7 @@ export type LogsDrilldownDefaultsOperatorState = { It is limited to three possible states for machine evaluation. */ state: 'success' | 'in_progress' | 'failed'; }; -export type LogsDrilldownDefaultsStatus = { +export type LogsDrilldownDefaultColumnsStatus = { /** additionalFields is reserved for future use */ additionalFields?: { [key: string]: { @@ -905,17 +1307,17 @@ export type LogsDrilldownDefaultsStatus = { /** 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]: LogsDrilldownDefaultsOperatorState; + [key: string]: LogsDrilldownDefaultColumnsOperatorState; }; }; -export type LogsDrilldownDefaults = { +export type LogsDrilldownDefaultColumns = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion: string; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind: string; metadata: ObjectMeta; - spec: LogsDrilldownDefaultsSpec; - status?: LogsDrilldownDefaultsStatus; + spec: LogsDrilldownDefaultColumnsSpec; + status?: LogsDrilldownDefaultColumnsStatus; }; 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. */ @@ -927,10 +1329,10 @@ export type ListMeta = { /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ selfLink?: string; }; -export type LogsDrilldownDefaultsList = { +export type LogsDrilldownDefaultColumnsList = { /** 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; - items: LogsDrilldownDefaults[]; + items: LogsDrilldownDefaultColumns[]; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; metadata: ListMeta; @@ -980,6 +1382,57 @@ export type Status = { status?: string; }; export type Patch = object; +export type LogsDrilldownDefaultsSpec = { + defaultFields: string[]; + interceptDismissed: boolean; + prettifyJSON: boolean; + wrapLogMessage: boolean; +}; +export type LogsDrilldownDefaultsOperatorState = { + /** 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]: { + [key: string]: any; + }; + }; + /** 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'; +}; +export type LogsDrilldownDefaultsStatus = { + /** additionalFields is reserved for future use */ + additionalFields?: { + [key: string]: { + [key: string]: any; + }; + }; + /** 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]: LogsDrilldownDefaultsOperatorState; + }; +}; +export type LogsDrilldownDefaults = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind: string; + metadata: ObjectMeta; + spec: LogsDrilldownDefaultsSpec; + status?: LogsDrilldownDefaultsStatus; +}; +export type LogsDrilldownDefaultsList = { + /** 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; + items: LogsDrilldownDefaults[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ListMeta; +}; export type LogsDrilldownSpec = { defaultFields: string[]; interceptDismissed: boolean; @@ -1034,6 +1487,19 @@ export type LogsDrilldownList = { export const { useGetApiResourcesQuery, useLazyGetApiResourcesQuery, + useListLogsDrilldownDefaultColumnsQuery, + useLazyListLogsDrilldownDefaultColumnsQuery, + useCreateLogsDrilldownDefaultColumnsMutation, + useDeletecollectionLogsDrilldownDefaultColumnsMutation, + useGetLogsDrilldownDefaultColumnsQuery, + useLazyGetLogsDrilldownDefaultColumnsQuery, + useReplaceLogsDrilldownDefaultColumnsMutation, + useDeleteLogsDrilldownDefaultColumnsMutation, + useUpdateLogsDrilldownDefaultColumnsMutation, + useGetLogsDrilldownDefaultColumnsStatusQuery, + useLazyGetLogsDrilldownDefaultColumnsStatusQuery, + useReplaceLogsDrilldownDefaultColumnsStatusMutation, + useUpdateLogsDrilldownDefaultColumnsStatusMutation, useListLogsDrilldownDefaultsQuery, useLazyListLogsDrilldownDefaultsQuery, useCreateLogsDrilldownDefaultsMutation, diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts index c4423173da3..3d82f571926 100644 --- a/packages/grafana-data/src/field/fieldDisplay.ts +++ b/packages/grafana-data/src/field/fieldDisplay.ts @@ -190,10 +190,62 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi y: dataFrame.fields[i], x: timeField, }; - if (calc === ReducerID.last) { - sparkline.highlightIndex = sparkline.y.values.length - 1; - } else if (calc === ReducerID.first) { - sparkline.highlightIndex = 0; + let highlightIdx: number | undefined = (() => { + switch (calc) { + case ReducerID.last: + return sparkline.y.values.length - 1; + case ReducerID.first: + return 0; + // TODO: #112977 enable more reducers for highlight index + // case ReducerID.lastNotNull: { + // for (let k = sparkline.y.values.length - 1; k >= 0; k--) { + // const v = sparkline.y.values[k]; + // if (v !== null && v !== undefined && !Number.isNaN(v)) { + // return k; + // } + // } + // return; + // } + // case ReducerID.firstNotNull: { + // for (let k = 0; k < sparkline.y.values.length; k++) { + // const v = sparkline.y.values[k]; + // if (v !== null && v !== undefined && !Number.isNaN(v)) { + // return k; + // } + // } + // return; + // } + // case ReducerID.min: { + // let minIdx = -1; + // let prevMin = Infinity; + // for (let k = 0; k < sparkline.y.values.length; k++) { + // const v = sparkline.y.values[k]; + // if (v !== null && v !== undefined && !Number.isNaN(v) && v < prevMin) { + // prevMin = v; + // minIdx = k; + // } + // } + // return minIdx >= 0 ? minIdx : undefined; + // } + // case ReducerID.max: { + // let maxIdx = -1; + // let prevMax = -Infinity; + // for (let k = 0; k < sparkline.y.values.length; k++) { + // const v = sparkline.y.values[k]; + // if (v !== null && v !== undefined && !Number.isNaN(v) && v > prevMax) { + // prevMax = v; + // maxIdx = k; + // } + // } + // return maxIdx >= 0 ? maxIdx : undefined; + // } + default: + return; + } + })(); + + if (typeof highlightIdx === 'number') { + sparkline.highlightIndex = highlightIdx; } } diff --git a/packages/grafana-data/src/field/fieldOverrides.test.ts b/packages/grafana-data/src/field/fieldOverrides.test.ts index 901fc6d8cc2..767da439543 100644 --- a/packages/grafana-data/src/field/fieldOverrides.test.ts +++ b/packages/grafana-data/src/field/fieldOverrides.test.ts @@ -253,7 +253,7 @@ describe('applyFieldOverrides', () => { ], }); - it('will apply field overrides to the fields within the frame', () => { + it('will apply default field overrides to the fields within the frame', () => { const f0 = createDataFrame({ name: 'A', fields: [ @@ -285,6 +285,41 @@ describe('applyFieldOverrides', () => { expect(withOverrides[0].fields[1].values[0].fields[1].state.range.max).toBe(30); }); + it('will apply targeted field overrides to the fields within the frame', () => { + const f0 = createDataFrame({ + name: 'A', + fields: [ + { + name: 'message', + type: FieldType.string, + values: ['foo'], + }, + { + name: 'frame', + type: FieldType.frame, + values: [f0Internal], + }, + ], + }); + const withOverrides = applyFieldOverrides({ + data: [f0], + fieldConfig: { + defaults: {}, + overrides: [ + { + matcher: { id: FieldMatcherID.byName, options: 'frame' }, + properties: [{ id: 'max', value: 30 }], + }, + ], + }, + replaceVariables: (value) => value, + theme: createTheme(), + fieldConfigRegistry: customFieldRegistry, + }); + + expect(withOverrides[0].fields[1].values[0].fields[1].config.max).toBe(30); + }); + it('will not crash when some of the nested frames are undefined', () => { const f0 = createDataFrame({ name: 'A', diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 2aac477cd2b..8b345036c64 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -1,4 +1,4 @@ -import { isNumber, set, unset, get, cloneDeep } from 'lodash'; +import { isNumber, set, unset, get, cloneDeep, defaultsDeep } from 'lodash'; import { createContext, useContext, useMemo, useRef } from 'react'; import { usePrevious } from 'react-use'; @@ -240,9 +240,14 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra ...options, // nested frames can be `undefined` in certain situations, like after `merge` transform due to padding the value array. // let's replace them with empty frames to avoid errors applying overrides - data: field.values.map( - (nestedFrame: DataFrame | undefined): DataFrame => nestedFrame ?? createDataFrame({ fields: [] }) - ), + data: field.values.map((nestedFrame: DataFrame | undefined): DataFrame => { + const result = nestedFrame ?? createDataFrame({ fields: [] }); + result.fields = result.fields.map((newField) => { + newField.config = defaultsDeep(newField.config || {}, config); + return newField; + }); + return result; + }), }); } } @@ -256,7 +261,7 @@ function calculateRange( field: Field, globalRange: NumericRange | undefined, data: DataFrame[] -): { range?: { min?: number | null; max?: number | null; delta: number }; newGlobalRange: NumericRange | undefined } { +): { range?: NumericRange; newGlobalRange?: NumericRange } { // If range is defined with min/max, use it if (isNumber(config.min) && isNumber(config.max)) { const range = { min: config.min, max: config.max, delta: config.max - config.min }; diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index 5084c577176..63c36639e25 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -521,6 +521,7 @@ export { VariableRefresh, VariableSort, VariableHide, + type VariableRegexApplyTo, type VariableType, type VariableModel, type TypedVariableModel, diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index eba76d2c198..4aeb394b81f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -261,6 +261,10 @@ export interface FeatureToggles { */ kubernetesCorrelations?: boolean; /** + * Adds support for Kubernetes unified storage quotas + */ + kubernetesUnifiedStorageQuotas?: boolean; + /** * Adds support for Kubernetes logs drilldown */ kubernetesLogsDrilldown?: boolean; @@ -984,6 +988,11 @@ export interface FeatureToggles { */ restoreDashboards?: boolean; /** + * Enables recently viewed dashboards section in the browsing dashboard page + * @default false + */ + recentlyViewedDashboards?: boolean; + /** * Enable configuration of alert enrichments in Grafana Cloud. * @default false */ @@ -1220,4 +1229,8 @@ export interface FeatureToggles { * @default false */ useMTPlugins?: boolean; + /** + * Enables support for variables whose values can have multiple properties + */ + multiPropsVariables?: boolean; } diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index b49be0d5363..34e672b66f5 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -288,6 +288,7 @@ export const availableIconsIndex = { bitbucket: true, git: true, 'tachometer-fast': true, + 'tachometer-empty': true, 'cmab-logo': true, }; diff --git a/packages/grafana-data/src/types/templateVars.ts b/packages/grafana-data/src/types/templateVars.ts index e6feea4dd3f..8b6ed69463b 100644 --- a/packages/grafana-data/src/types/templateVars.ts +++ b/packages/grafana-data/src/types/templateVars.ts @@ -32,6 +32,8 @@ export enum VariableRefresh { onTimeRangeChanged, } +export type VariableRegexApplyTo = 'value' | 'text'; + export enum VariableSort { disabled, alphabeticalAsc, @@ -117,6 +119,7 @@ export interface QueryVariableModel extends VariableWithMultiSupport { queryValue?: string; query: any; regex: string; + regexApplyTo?: VariableRegexApplyTo; refresh: VariableRefresh; staticOptions?: VariableOption[]; staticOptionsOrder?: 'before' | 'after' | 'sorted'; diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 1fa640a2563..b88dac9099c 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -508,6 +508,9 @@ export const versionedPages = { queryOptionsRegExInputV2: { [MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Query RegEx field', }, + queryOptionsRegExApplyToSelectV2: { + [MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Query RegExApplyTo select', + }, queryOptionsSortSelect: { [MIN_GRAFANA_VERSION]: 'Variable editor Form Query Sort select', }, diff --git a/packages/grafana-prometheus/src/constants.ts b/packages/grafana-prometheus/src/constants.ts index 24bc613de60..053180f4a13 100644 --- a/packages/grafana-prometheus/src/constants.ts +++ b/packages/grafana-prometheus/src/constants.ts @@ -1,8 +1,4 @@ // Max number of items (metrics, labels, values) that we display as suggestions. Prevents from running out of memory. -/** - * @deprecated - */ -export const SUGGESTIONS_LIMIT = 10000; export const PROMETHEUS_QUERY_BUILDER_MAX_RESULTS = 1000; @@ -45,8 +41,3 @@ export const GET_AND_POST_METADATA_ENDPOINTS = [ 'api/v1/labels', 'suggestions', ]; - -/** - * @deprecated - */ -export const REMOVE_SERIES_LIMIT = 'none'; diff --git a/packages/grafana-prometheus/src/index.ts b/packages/grafana-prometheus/src/index.ts index d0539d46591..f6950cc9fb7 100644 --- a/packages/grafana-prometheus/src/index.ts +++ b/packages/grafana-prometheus/src/index.ts @@ -86,7 +86,6 @@ export { PrometheusVariableSupport } from './variables'; export type { PrometheusLanguageProviderInterface } from './language_provider'; // For Metrics Drilldown -export { default as PromQlLanguageProvider } from './language_provider'; export { getPrometheusTime } from './language_utils'; export { isValidLegacyName, utf8Support, wrapUtf8Filters } from './utf8_support'; export { buildVisualQueryFromString } from './querybuilder/parsing'; diff --git a/packages/grafana-prometheus/src/language_provider.mock.ts b/packages/grafana-prometheus/src/language_provider.mock.ts index 09e16e5a9af..adf97211cb6 100644 --- a/packages/grafana-prometheus/src/language_provider.mock.ts +++ b/packages/grafana-prometheus/src/language_provider.mock.ts @@ -10,16 +10,6 @@ export class EmptyLanguageProviderMock { }); } - getLabelKeys = jest.fn().mockReturnValue([]); - getLabelValues = jest.fn().mockReturnValue([]); - getSeries = jest.fn().mockReturnValue({ __name__: [] }); - getSeriesValues = jest.fn().mockReturnValue([]); - fetchSeries = jest.fn().mockReturnValue([]); - fetchSeriesLabels = jest.fn().mockReturnValue([]); - fetchSeriesLabelsMatch = jest.fn().mockReturnValue([]); - fetchLabelsWithMatch = jest.fn().mockReturnValue([]); - fetchLabels = jest.fn(); - loadMetricsMetadata = jest.fn(); retrieveMetrics = jest.fn().mockReturnValue(['metric']); queryLabelKeys = jest.fn().mockResolvedValue([]); queryLabelValues = jest.fn().mockResolvedValue([]); diff --git a/packages/grafana-prometheus/src/language_provider.test.ts b/packages/grafana-prometheus/src/language_provider.test.ts index a3c32d7f881..6ddb6188117 100644 --- a/packages/grafana-prometheus/src/language_provider.test.ts +++ b/packages/grafana-prometheus/src/language_provider.test.ts @@ -1,26 +1,24 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/language_provider.test.ts import { AbstractLabelOperator, dateTime, TimeRange } from '@grafana/data'; +import { getCacheDurationInMinutes } from './caching'; +import { DEFAULT_SERIES_LIMIT } from './constants'; +import { PrometheusDatasource } from './datasource'; +import { + exportToAbstractQuery, + importFromAbstractQuery, + populateMatchParamsFromQueries, + PrometheusLanguageProvider, +} from './language_provider'; +import { getPrometheusTime, getRangeSnapInterval } from './language_utils'; +import { PrometheusCacheLevel, PromQuery } from './types'; + jest.mock('./language_utils', () => ({ ...jest.requireActual('./language_utils'), getPrometheusTime: jest.requireActual('./language_utils').getPrometheusTime, getRangeSnapInterval: jest.requireActual('./language_utils').getRangeSnapInterval, })); -import { getCacheDurationInMinutes } from './caching'; -import { Label } from './components/monaco-query-field/monaco-completion-provider/situation'; -import { DEFAULT_SERIES_LIMIT } from './constants'; -import { PrometheusDatasource } from './datasource'; -import { - exportToAbstractQuery, - importFromAbstractQuery, - PrometheusLanguageProviderInterface, - PrometheusLanguageProvider, - populateMatchParamsFromQueries, -} from './language_provider'; -import { getPrometheusTime, getRangeSnapInterval } from './language_utils'; -import { PrometheusCacheLevel, PromQuery } from './types'; - const now = new Date(1681300293392).getTime(); const timeRangeDurationSeconds = 1; const toPrometheusTime = getPrometheusTime(dateTime(now), false); @@ -58,607 +56,7 @@ const getMockQuantizedTimeRangeParams = (override?: Partial): TimeRan ...override, }); -// Common test helper to verify request parameters -const verifyRequestParams = ( - requestSpy: jest.SpyInstance, - expectedUrl: string, - expectedParams: unknown, - expectedOptions?: unknown -) => { - expect(requestSpy).toHaveBeenCalled(); - expect(requestSpy).toHaveBeenCalledWith(expectedUrl, expect.objectContaining(expectedParams), expectedOptions); -}; - -describe('Prometheus Language Provider', () => { - const defaultDatasource: PrometheusDatasource = { - metadataRequest: () => ({ data: { data: [] } }), - getTimeRangeParams: getTimeRangeParams, - interpolateString: (string: string) => string, - hasLabelsMatchAPISupport: () => false, - getDaysToCacheMetadata: () => 1, - getAdjustedInterval: () => getRangeSnapInterval(PrometheusCacheLevel.None, getMockQuantizedTimeRangeParams()), - cacheLevel: PrometheusCacheLevel.None, - getIntervalVars: () => ({}), - getRangeScopedVars: () => ({}), - seriesLimit: DEFAULT_SERIES_LIMIT, - } as unknown as PrometheusDatasource; - - describe('Series and label fetching', () => { - const timeRange = getMockTimeRange(); - - describe('getSeries', () => { - it('should use fetchDefaultSeries for empty selector', async () => { - const languageProvider = new PrometheusLanguageProvider(defaultDatasource); - const fetchDefaultSeriesSpy = jest.spyOn(languageProvider, 'fetchDefaultSeries'); - fetchDefaultSeriesSpy.mockResolvedValue({ job: ['job1', 'job2'], instance: ['instance1', 'instance2'] }); - - const result = await languageProvider.getSeries(timeRange, '{}'); - - expect(fetchDefaultSeriesSpy).toHaveBeenCalledWith(timeRange); - expect(result).toEqual({ job: ['job1', 'job2'], instance: ['instance1', 'instance2'] }); - }); - - it('should use fetchSeriesLabels for non-empty selector', async () => { - const languageProvider = new PrometheusLanguageProvider(defaultDatasource); - const fetchSeriesLabelsSpy = jest.spyOn(languageProvider, 'fetchSeriesLabels'); - fetchSeriesLabelsSpy.mockResolvedValue({ job: ['job1', 'job2'], instance: ['instance1', 'instance2'] }); - - const result = await languageProvider.getSeries(timeRange, '{job="grafana"}'); - - expect(fetchSeriesLabelsSpy).toHaveBeenCalledWith(timeRange, '{job="grafana"}', undefined, 'none'); - expect(result).toEqual({ job: ['job1', 'job2'], instance: ['instance1', 'instance2'] }); - }); - - it('should include name label when withName is true', async () => { - const languageProvider = new PrometheusLanguageProvider(defaultDatasource); - const fetchSeriesLabelsSpy = jest.spyOn(languageProvider, 'fetchSeriesLabels'); - fetchSeriesLabelsSpy.mockResolvedValue({ __name__: ['metric1', 'metric2'], job: ['job1'] }); - - const result = await languageProvider.getSeries(timeRange, '{job="grafana"}', true); - - expect(fetchSeriesLabelsSpy).toHaveBeenCalledWith(timeRange, '{job="grafana"}', true, 'none'); - expect(result).toHaveProperty('__name__'); - expect(result.__name__).toEqual(['metric1', 'metric2']); - }); - - it('should handle errors gracefully', async () => { - jest.spyOn(console, 'error').mockImplementation(); - const languageProvider = new PrometheusLanguageProvider(defaultDatasource); - jest.spyOn(languageProvider, 'fetchSeriesLabels').mockRejectedValue(new Error('Network error')); - - const result = await languageProvider.getSeries(timeRange, '{job="grafana"}'); - - expect(result).toEqual({}); - }); - }); - - describe('getSeriesLabels', () => { - it('should call labels endpoint when API support is available', () => { - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - hasLabelsMatchAPISupport: () => true, - } as PrometheusDatasource); - const getSeriesLabels = languageProvider.getSeriesLabels; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - const labelName = 'job'; - const labelValue = 'grafana'; - getSeriesLabels(timeRange, `{${labelName}="${labelValue}"}`, [ - { - name: labelName, - value: labelValue, - op: '=', - }, - ] as Label[]); - - verifyRequestParams(requestSpy, '/api/v1/labels', { - end: toPrometheusTimeString, - 'match[]': '{job="grafana"}', - start: fromPrometheusTimeString, - }); - }); - - it('should call series endpoint when API support is not available', () => { - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - getAdjustedInterval: (_: TimeRange) => - getRangeSnapInterval(PrometheusCacheLevel.None, getMockQuantizedTimeRangeParams()), - } as PrometheusDatasource); - const getSeriesLabels = languageProvider.getSeriesLabels; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - const labelName = 'job'; - const labelValue = 'grafana'; - getSeriesLabels(timeRange, `{${labelName}="${labelValue}"}`, [ - { - name: labelName, - value: labelValue, - op: '=', - }, - ] as Label[]); - - verifyRequestParams(requestSpy, '/api/v1/series', { - end: toPrometheusTimeString, - 'match[]': '{job="grafana"}', - start: fromPrometheusTimeString, - }); - }); - - it('should call labels endpoint with quantized time parameters when cache level is set', () => { - const timeSnapMinutes = getCacheDurationInMinutes(PrometheusCacheLevel.Low); - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - hasLabelsMatchAPISupport: () => true, - cacheLevel: PrometheusCacheLevel.Low, - getAdjustedInterval: (_: TimeRange) => - getRangeSnapInterval(PrometheusCacheLevel.Low, getMockQuantizedTimeRangeParams()), - } as PrometheusDatasource); - const getSeriesLabels = languageProvider.getSeriesLabels; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - const labelName = 'job'; - const labelValue = 'grafana'; - getSeriesLabels(timeRange, `{${labelName}="${labelValue}"}`, [ - { - name: labelName, - value: labelValue, - op: '=', - }, - ] as Label[]); - - expect(requestSpy).toHaveBeenCalled(); - expect(requestSpy).toHaveBeenCalledWith( - '/api/v1/labels', - { - end: ( - dateTime(fromPrometheusTime * 1000) - .add(timeSnapMinutes, 'minute') - .startOf('minute') - .valueOf() / 1000 - ).toString(), - 'match[]': '{job="grafana"}', - start: ( - dateTime(toPrometheusTime * 1000) - .startOf('minute') - .valueOf() / 1000 - ).toString(), - }, - { headers: { 'X-Grafana-Cache': `private, max-age=${timeSnapMinutes * 60}` } } - ); - }); - }); - - describe('getSeriesValues', () => { - it('should call series endpoint when labels match API is not supported', () => { - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - } as PrometheusDatasource); - const getSeriesValues = languageProvider.getSeriesValues; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - getSeriesValues(timeRange, 'job', '{job="grafana"}'); - - verifyRequestParams(requestSpy, '/api/v1/series', { - end: toPrometheusTimeString, - 'match[]': '{job="grafana"}', - start: fromPrometheusTimeString, - }); - }); - - it('should call label values endpoint when labels match API is supported', () => { - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - hasLabelsMatchAPISupport: () => true, - } as PrometheusDatasource); - const getSeriesValues = languageProvider.getSeriesValues; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - const labelName = 'job'; - const labelValue = 'grafana'; - getSeriesValues(timeRange, labelName, `{${labelName}="${labelValue}"}`); - - verifyRequestParams(requestSpy, `/api/v1/label/${labelName}/values`, { - end: toPrometheusTimeString, - 'match[]': `{${labelName}="${labelValue}"}`, - start: fromPrometheusTimeString, - }); - }); - - it('should properly interpolate template variables in queries', () => { - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - interpolateString: (string: string) => string.replace(/\$/g, 'interpolated-'), - } as PrometheusDatasource); - const getSeriesValues = languageProvider.getSeriesValues; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - getSeriesValues(timeRange, 'job', '{instance="$instance", job="grafana"}'); - - verifyRequestParams(requestSpy, '/api/v1/series', { - end: toPrometheusTimeString, - 'match[]': '{instance="interpolated-instance", job="grafana"}', - start: fromPrometheusTimeString, - }); - }); - }); - - describe('fetchSeries', () => { - it('should use match[] parameter in request', async () => { - const languageProvider = new PrometheusLanguageProvider(defaultDatasource); - await languageProvider.start(timeRange); - const requestSpy = jest.spyOn(languageProvider, 'request'); - - await languageProvider.fetchSeries(timeRange, '{job="grafana"}'); - - verifyRequestParams(requestSpy, '/api/v1/series', { - end: toPrometheusTimeString, - 'match[]': '{job="grafana"}', - start: fromPrometheusTimeString, - }); - }); - }); - - describe('fetchSeriesLabels', () => { - it('should interpolate variables in series queries', () => { - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - interpolateString: (string: string) => string.replace(/\$/g, 'interpolated-'), - } as PrometheusDatasource); - const fetchSeriesLabels = languageProvider.fetchSeriesLabels; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - fetchSeriesLabels(getMockTimeRange(), '$metric', undefined, DEFAULT_SERIES_LIMIT); - - verifyRequestParams(requestSpy, '/api/v1/series', { - end: toPrometheusTimeString, - 'match[]': 'interpolated-metric', - start: fromPrometheusTimeString, - limit: DEFAULT_SERIES_LIMIT, - }); - }); - - it('should not include limit parameter when "none" is specified', () => { - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - } as PrometheusDatasource); - const fetchSeriesLabels = languageProvider.fetchSeriesLabels; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - fetchSeriesLabels(getMockTimeRange(), 'metric-with-limit', undefined, 'none'); - - verifyRequestParams(requestSpy, '/api/v1/series', { - end: toPrometheusTimeString, - 'match[]': 'metric-with-limit', - start: fromPrometheusTimeString, - }); - }); - }); - }); - - describe('fetchLabels API', () => { - const tr = getMockTimeRange(); - - const getParams = (requestSpy: ReturnType) => { - return requestSpy.mock.calls[0][1]?.toString() ?? 'undefined'; - }; - - describe('with POST method', () => { - let languageProvider: PrometheusLanguageProviderInterface; - - beforeEach(() => { - languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - httpMethod: 'POST', - } as PrometheusDatasource); - }); - - it('should send single metric to request', async () => { - const mockQueries: PromQuery[] = [{ refId: 'C', expr: 'go_gc_pauses_seconds_bucket' }]; - const fetchLabel = languageProvider.fetchLabels; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - await fetchLabel(tr, mockQueries); - - expect(requestSpy).toHaveBeenCalled(); - const params = getParams(requestSpy); - expect(params).toMatch(encodeURI('match[]=go_gc_pauses_seconds_bucket')); - }); - - it('should extract metrics from complex PromQL expressions', async () => { - const mockQueries: PromQuery[] = [ - { - refId: 'C', - expr: 'histogram_quantile(0.95, sum(rate(go_gc_pauses_seconds_bucket[$__rate_interval])) by (le))', - }, - ]; - const fetchLabel = languageProvider.fetchLabels; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - await fetchLabel(tr, mockQueries); - - expect(requestSpy).toHaveBeenCalled(); - const params = getParams(requestSpy); - expect(params).toMatch(encodeURI('match[]=go_gc_pauses_seconds_bucket')); - }); - - it('should combine metrics from multiple queries', async () => { - const mockQueries: PromQuery[] = [ - { refId: 'B', expr: 'process_cpu_seconds_total' }, - { refId: 'C', expr: 'go_gc_pauses_seconds_bucket' }, - ]; - const fetchLabel = languageProvider.fetchLabels; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - await fetchLabel(tr, mockQueries); - - expect(requestSpy).toHaveBeenCalled(); - const params = getParams(requestSpy); - expect(params).toMatch(encodeURI('match[]=process_cpu_seconds_total&match[]=go_gc_pauses_seconds_bucket')); - }); - - it('should extract multiple metrics from binary operations', async () => { - const mockQueries: PromQuery[] = [ - { refId: 'B', expr: 'process_cpu_seconds_total + go_gc_pauses_seconds_bucket' }, - ]; - const fetchLabel = languageProvider.fetchLabels; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - await fetchLabel(tr, mockQueries); - - expect(requestSpy).toHaveBeenCalled(); - const params = getParams(requestSpy); - expect(params).toMatch(encodeURI('match[]=process_cpu_seconds_total&match[]=go_gc_pauses_seconds_bucket')); - }); - - it('should set and return labelKeys from API response', async () => { - const mockQueries: PromQuery[] = [{ refId: 'C', expr: 'go_gc_pauses_seconds_bucket' }]; - const fetchLabel = languageProvider.fetchLabels; - const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['foo', 'bar']); - - const keys = await fetchLabel(tr, mockQueries); - - expect(requestSpy).toHaveBeenCalled(); - expect(languageProvider.labelKeys).toEqual(['bar', 'foo']); // Sorted order - expect(keys).toEqual(['bar', 'foo']); - }); - }); - - describe('with GET method', () => { - let languageProvider: PrometheusLanguageProviderInterface; - - beforeEach(() => { - languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - httpMethod: 'GET', - } as PrometheusDatasource); - }); - - it('should send query metrics in URL for GET requests', async () => { - const mockQueries: PromQuery[] = [{ refId: 'C', expr: 'go_gc_pauses_seconds_bucket' }]; - const fetchLabel = languageProvider.fetchLabels; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - await fetchLabel(tr, mockQueries); - - expect(requestSpy).toHaveBeenCalled(); - expect(requestSpy.mock.calls[0][0]).toMatch(encodeURI('match[]=go_gc_pauses_seconds_bucket')); - }); - - it('should handle empty queries correctly', async () => { - const mockQueries: PromQuery[] = [{ refId: 'A', expr: '' }]; - const fetchLabel = languageProvider.fetchLabels; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - await fetchLabel(tr, mockQueries); - - expect(requestSpy).toHaveBeenCalled(); - expect(requestSpy.mock.calls[0][0].indexOf('match[]')).toEqual(-1); - }); - }); - }); - - describe('Label value handling', () => { - describe('fetchLabelValues', () => { - it('should interpolate variables in labels', () => { - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - interpolateString: (string: string) => string.replace(/\$/g, 'interpolated_'), - } as PrometheusDatasource); - const fetchLabelValues = languageProvider.fetchLabelValues; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - fetchLabelValues(getMockTimeRange(), '$job'); - - verifyRequestParams(requestSpy, '/api/v1/label/interpolated_job/values', { - end: toPrometheusTimeString, - start: fromPrometheusTimeString, - }); - }); - - it('should properly encode UTF-8 labels', () => { - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - interpolateString: (string: string) => string.replace(/\$/g, 'http.status:sum'), - } as PrometheusDatasource); - const fetchLabelValues = languageProvider.fetchLabelValues; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - fetchLabelValues(getMockTimeRange(), '"http.status:sum"'); - - verifyRequestParams(requestSpy, '/api/v1/label/U__http_2e_status:sum/values', { - end: toPrometheusTimeString, - start: fromPrometheusTimeString, - }); - }); - - it('should handle special characters safely in label values', () => { - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - interpolateString: (string: string) => string.replace(/\$/g, 'value with spaces & special chars'), - } as PrometheusDatasource); - const fetchLabelValues = languageProvider.fetchLabelValues; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - fetchLabelValues(getMockTimeRange(), '$job'); - - expect(requestSpy).toHaveBeenCalled(); - expect(requestSpy.mock.calls[0][0]).not.toContain(' '); - expect(requestSpy.mock.calls[0][0]).not.toContain('&'); - }); - }); - - describe('fetchSeriesValuesWithMatch', () => { - it('should handle UTF-8 encoding for special label names', () => { - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - interpolateString: (string: string) => string.replace(/\$/g, 'http.status:sum'), - } as PrometheusDatasource); - const fetchSeriesValuesWithMatch = languageProvider.fetchSeriesValuesWithMatch; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - fetchSeriesValuesWithMatch(getMockTimeRange(), '"http.status:sum"', '{__name__="a_utf8_http_requests_total"}'); - - verifyRequestParams(requestSpy, '/api/v1/label/U__http_2e_status:sum/values', { - end: toPrometheusTimeString, - start: fromPrometheusTimeString, - 'match[]': '{__name__="a_utf8_http_requests_total"}', - }); - }); - - it('should not encode standard Prometheus label names', () => { - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - } as PrometheusDatasource); - const fetchSeriesValuesWithMatch = languageProvider.fetchSeriesValuesWithMatch; - const requestSpy = jest.spyOn(languageProvider, 'request'); - - fetchSeriesValuesWithMatch(getMockTimeRange(), '"http_status_sum"', '{__name__="a_utf8_http_requests_total"}'); - - verifyRequestParams(requestSpy, '/api/v1/label/http_status_sum/values', { - end: toPrometheusTimeString, - start: fromPrometheusTimeString, - 'match[]': '{__name__="a_utf8_http_requests_total"}', - }); - }); - }); - }); - - describe('fetchSuggestions', () => { - it('should send POST request with correct parameters', async () => { - const timeRange = getMockTimeRange(); - const mockQueries: PromQuery[] = [{ refId: 'A', expr: 'metric1' }]; - - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - interpolateString: (string: string) => `interpolated_${string}`, - getIntervalVars: () => ({ __interval: '1m' }), - getRangeScopedVars: () => ({ __range: { text: '1h', value: '1h' } }), - } as unknown as PrometheusDatasource); - - const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['suggestion1', 'suggestion2']); - - // Simplifying the test by not passing complex scope objects that require more type definitions - const result = await languageProvider.fetchSuggestions( - timeRange, - mockQueries, - undefined, // omitting scopes parameter - [{ key: 'instance', operator: '=', value: 'localhost' }], - 'metric', - 100 - ); - - expect(requestSpy).toHaveBeenCalled(); - expect(requestSpy.mock.calls[0][0]).toBe('/suggestions'); - - // Check method and content type - expect(requestSpy.mock.calls[0][2]).toMatchObject({ - headers: { 'Content-Type': 'application/json' }, - method: 'POST', - }); - - // Check query parameters - expect(requestSpy.mock.calls[0][1]).toMatchObject({ - labelName: 'metric', - limit: 100, - queries: ['interpolated_metric1'], - }); - - expect(result).toEqual(['suggestion1', 'suggestion2']); - }); - - it('should use default time range if not provided', async () => { - const languageProvider = new PrometheusLanguageProvider(defaultDatasource); - const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['result']); - - await languageProvider.fetchSuggestions(undefined, [], [], [], 'test'); - - expect(requestSpy).toHaveBeenCalled(); - // Default time range should be used - expect(requestSpy.mock.calls[0][1]).toHaveProperty('start'); - expect(requestSpy.mock.calls[0][1]).toHaveProperty('end'); - }); - - it('should handle empty response gracefully', async () => { - const languageProvider = new PrometheusLanguageProvider(defaultDatasource); - jest.spyOn(languageProvider, 'request').mockResolvedValue(null); - - const result = await languageProvider.fetchSuggestions(getMockTimeRange(), [], [], [], 'test'); - - expect(result).toEqual([]); - }); - - it('should include cache headers when cacheLevel is set', async () => { - const timeSnapMinutes = getCacheDurationInMinutes(PrometheusCacheLevel.Medium); - const languageProvider = new PrometheusLanguageProvider({ - ...defaultDatasource, - cacheLevel: PrometheusCacheLevel.Medium, - } as PrometheusDatasource); - - const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['result']); - - await languageProvider.fetchSuggestions(getMockTimeRange(), [], [], [], 'test'); - - expect(requestSpy).toHaveBeenCalled(); - expect(requestSpy.mock.calls[0][2]?.headers).toHaveProperty('X-Grafana-Cache'); - expect(requestSpy.mock.calls[0][2]?.headers?.['X-Grafana-Cache']).toContain( - `private, max-age=${timeSnapMinutes * 60}` - ); - }); - }); -}); - -describe('Query transformation', () => { - describe('importFromAbstractQuery', () => { - it('should handle empty queries', async () => { - const result = importFromAbstractQuery({ refId: 'bar', labelMatchers: [] }); - expect(result).toEqual({ refId: 'bar', expr: '', range: true }); - }); - }); - - describe('exportToAbstractQuery', () => { - it('should extract labels and metric name from PromQL', async () => { - const abstractQuery = exportToAbstractQuery({ - refId: 'bar', - expr: 'metric_name{label1="value1", label2!="value2", label3=~"value3", label4!~"value4"}', - instant: true, - range: false, - }); - - expect(abstractQuery).toMatchObject({ - refId: 'bar', - labelMatchers: [ - { name: 'label1', operator: AbstractLabelOperator.Equal, value: 'value1' }, - { name: 'label2', operator: AbstractLabelOperator.NotEqual, value: 'value2' }, - { name: 'label3', operator: AbstractLabelOperator.EqualRegEx, value: 'value3' }, - { name: 'label4', operator: AbstractLabelOperator.NotEqualRegEx, value: 'value4' }, - { name: '__name__', operator: AbstractLabelOperator.Equal, value: 'metric_name' }, - ], - }); - }); - }); -}); - -describe('PrometheusLanguageProvider with feature toggle', () => { +describe('PrometheusLanguageProvider', () => { const defaultDatasource: PrometheusDatasource = { metadataRequest: () => ({ data: { data: [] } }), getTimeRangeParams: getTimeRangeParams, @@ -712,7 +110,6 @@ describe('PrometheusLanguageProvider with feature toggle', () => { expect(resourceClientStartSpy).toHaveBeenCalled(); expect(queryMetadataSpy).toHaveBeenCalled(); expect(provider.retrieveMetricsMetadata()).toEqual(mockMetadata); - expect(provider.metricsMetadata).toEqual(mockMetadata); // Check backward compatibility }); it('should call queryMetricsMetadata with datasource seriesLimit during start', async () => { @@ -755,6 +152,7 @@ describe('PrometheusLanguageProvider with feature toggle', () => { const customLimit = 1000; const queryMetadataSpy = jest.spyOn(provider as any, '_queryMetadata').mockResolvedValue(mockMetadata); + // TODO spy on /api/v1/metadata endpoint or fetch const result = await provider.queryMetricsMetadata(customLimit); expect(queryMetadataSpy).toHaveBeenCalledWith(customLimit); @@ -1034,4 +432,118 @@ describe('PrometheusLanguageProvider with feature toggle', () => { expect(result).toEqual(['__name__=~"go_cpu_classes_idle_cpu_seconds_total"']); }); }); + + describe('fetchSuggestions', () => { + it('should send POST request with correct parameters', async () => { + const timeRange = getMockTimeRange(); + const mockQueries: PromQuery[] = [{ refId: 'A', expr: 'metric1' }]; + + const languageProvider = new PrometheusLanguageProvider({ + ...defaultDatasource, + interpolateString: (string: string) => `interpolated_${string}`, + getIntervalVars: () => ({ __interval: '1m' }), + getRangeScopedVars: () => ({ __range: { text: '1h', value: '1h' } }), + } as unknown as PrometheusDatasource); + + const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['suggestion1', 'suggestion2']); + + // Simplifying the test by not passing complex scope objects that require more type definitions + const result = await languageProvider.fetchSuggestions( + timeRange, + mockQueries, + undefined, // omitting scopes parameter + [{ key: 'instance', operator: '=', value: 'localhost' }], + 'metric', + 100 + ); + + expect(requestSpy).toHaveBeenCalled(); + expect(requestSpy.mock.calls[0][0]).toBe('/suggestions'); + + // Check method and content type + expect(requestSpy.mock.calls[0][2]).toMatchObject({ + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + }); + + // Check query parameters + expect(requestSpy.mock.calls[0][1]).toMatchObject({ + labelName: 'metric', + limit: 100, + queries: ['interpolated_metric1'], + }); + + expect(result).toEqual(['suggestion1', 'suggestion2']); + }); + + it('should use default time range if not provided', async () => { + const languageProvider = new PrometheusLanguageProvider(defaultDatasource); + const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['result']); + + await languageProvider.fetchSuggestions(undefined, [], [], [], 'test'); + + expect(requestSpy).toHaveBeenCalled(); + // Default time range should be used + expect(requestSpy.mock.calls[0][1]).toHaveProperty('start'); + expect(requestSpy.mock.calls[0][1]).toHaveProperty('end'); + }); + + it('should handle empty response gracefully', async () => { + const languageProvider = new PrometheusLanguageProvider(defaultDatasource); + jest.spyOn(languageProvider, 'request').mockResolvedValue(null); + + const result = await languageProvider.fetchSuggestions(getMockTimeRange(), [], [], [], 'test'); + + expect(result).toEqual([]); + }); + + it('should include cache headers when cacheLevel is set', async () => { + const timeSnapMinutes = getCacheDurationInMinutes(PrometheusCacheLevel.Medium); + const languageProvider = new PrometheusLanguageProvider({ + ...defaultDatasource, + cacheLevel: PrometheusCacheLevel.Medium, + } as PrometheusDatasource); + + const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue(['result']); + + await languageProvider.fetchSuggestions(getMockTimeRange(), [], [], [], 'test'); + + expect(requestSpy).toHaveBeenCalled(); + expect(requestSpy.mock.calls[0][2]?.headers).toHaveProperty('X-Grafana-Cache'); + expect(requestSpy.mock.calls[0][2]?.headers?.['X-Grafana-Cache']).toContain( + `private, max-age=${timeSnapMinutes * 60}` + ); + }); + }); +}); + +describe('Query transformation', () => { + describe('importFromAbstractQuery', () => { + it('should handle empty queries', async () => { + const result = importFromAbstractQuery({ refId: 'bar', labelMatchers: [] }); + expect(result).toEqual({ refId: 'bar', expr: '', range: true }); + }); + }); + + describe('exportToAbstractQuery', () => { + it('should extract labels and metric name from PromQL', async () => { + const abstractQuery = exportToAbstractQuery({ + refId: 'bar', + expr: 'metric_name{label1="value1", label2!="value2", label3=~"value3", label4!~"value4"}', + instant: true, + range: false, + }); + + expect(abstractQuery).toMatchObject({ + refId: 'bar', + labelMatchers: [ + { name: 'label1', operator: AbstractLabelOperator.Equal, value: 'value1' }, + { name: 'label2', operator: AbstractLabelOperator.NotEqual, value: 'value2' }, + { name: 'label3', operator: AbstractLabelOperator.EqualRegEx, value: 'value3' }, + { name: 'label4', operator: AbstractLabelOperator.NotEqualRegEx, value: 'value4' }, + { name: '__name__', operator: AbstractLabelOperator.Equal, value: 'metric_name' }, + ], + }); + }); + }); }); diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index 568acddba64..0daf12d7b7e 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -1,5 +1,4 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/language_provider.ts -import { once } from 'lodash'; import Prism from 'prismjs'; import { @@ -8,7 +7,6 @@ import { AbstractQuery, AdHocVariableFilter, getDefaultTimeRange, - LanguageProvider, Scope, scopeFilterOperatorMap, ScopeSpecFilter, @@ -17,34 +15,12 @@ import { import { BackendSrvRequest } from '@grafana/runtime'; import { buildCacheHeaders, getDaysToCacheMetadata, getDefaultCacheHeaders } from './caching'; -import { Label } from './components/monaco-query-field/monaco-completion-provider/situation'; -import { DEFAULT_SERIES_LIMIT, EMPTY_SELECTOR, REMOVE_SERIES_LIMIT } from './constants'; import { PrometheusDatasource } from './datasource'; -import { - extractLabelMatchers, - fixSummariesMetadata, - processHistogramMetrics, - processLabels, - removeQuotesIfExist, - toPromLikeQuery, -} from './language_utils'; +import { extractLabelMatchers, fixSummariesMetadata, toPromLikeQuery } from './language_utils'; import { promqlGrammar } from './promql'; import { buildVisualQueryFromString } from './querybuilder/parsing'; import { LabelsApiClient, ResourceApiClient, SeriesApiClient } from './resource_clients'; import { PromMetricsMetadata, PromQuery } from './types'; -import { escapeForUtf8Support, isValidLegacyName } from './utf8_support'; - -const DEFAULT_KEYS = ['job', 'instance']; - -/** - * Prometheus API endpoints for fetching resources - */ -const API_V1 = { - METADATA: '/api/v1/metadata', - SERIES: '/api/v1/series', - LABELS: '/api/v1/labels', - LABELS_VALUES: (labelKey: string) => `/api/v1/label/${labelKey}/values`, -}; interface PrometheusBaseLanguageProvider { datasource: PrometheusDatasource; @@ -53,7 +29,7 @@ interface PrometheusBaseLanguageProvider { * When no timeRange provided, we will use the default time range (now/now-6h) * @param timeRange */ - start: (timeRange?: TimeRange) => Promise; + start: (timeRange?: TimeRange) => Promise; request: (url: string, params?: any, options?: Partial) => Promise; @@ -68,455 +44,6 @@ interface PrometheusBaseLanguageProvider { ) => Promise; } -/** - * @deprecated This interface is deprecated and will be removed. - */ -interface PrometheusLegacyLanguageProvider { - /** - * @deprecated Use retrieveHistogramMetrics() method instead - */ - histogramMetrics: string[]; - /** - * @deprecated Use retrieveMetrics() method instead - */ - metrics: string[]; - /** - * @deprecated Use retrieveMetricsMetadata() method instead - */ - metricsMetadata?: PromMetricsMetadata; - /** - * @deprecated Use retrieveLabelKeys() method instead - */ - labelKeys: string[]; - - /** - * @deprecated Use queryMetricsMetadata() method instead. - */ - loadMetricsMetadata: () => void; - /** - * @deprecated Use retrieveMetricsMetadata() method instead - */ - getLabelKeys: () => string[]; - /** - * @deprecated If you need labelKeys or labelValues please use queryLabelKeys() or queryLabelValues() functions - */ - getSeries: (timeRange: TimeRange, selector: string, withName?: boolean) => Promise>; - /** - * @deprecated Use queryLabelValues() method insteadIt'll determine the right endpoint based on the datasource settings - */ - fetchLabelValues: (range: TimeRange, key: string, limit?: string | number) => Promise; - /** - * @deprecated Use queryLabelValues() method insteadIt'll determine the right endpoint based on the datasource settings - */ - getLabelValues: (range: TimeRange, key: string) => Promise; - /** - * @deprecated If you need labelKeys or labelValues please use queryLabelKeys() or queryLabelValues() functions - */ - fetchLabels: (timeRange: TimeRange, queries?: PromQuery[], limit?: string) => Promise; - /** - * @deprecated Use queryLabelValues() method insteadIt'll determine the right endpoint based on the datasource settings - */ - getSeriesValues: (timeRange: TimeRange, labelName: string, selector: string) => Promise; - /** - * @deprecated Use queryLabelValues() method insteadIt'll determine the right endpoint based on the datasource settings - */ - fetchSeriesValuesWithMatch: ( - timeRange: TimeRange, - name: string, - match?: string, - requestId?: string, - withLimit?: string | number - ) => Promise; - /** - * @deprecated Use queryLabelKeys() method instead. It'll determine the right endpoint based on the datasource settings - */ - getSeriesLabels: (timeRange: TimeRange, selector: string, otherLabels: Label[]) => Promise; - /** - * @deprecated Use queryLabelKeys() method instead. It'll determine the right endpoint based on the datasource settings - */ - fetchLabelsWithMatch: ( - timeRange: TimeRange, - name: string, - withName?: boolean, - withLimit?: string | number - ) => Promise>; - /** - * @deprecated Use queryLabelKeys() method instead. It'll determine the right endpoint based on the datasource settings - */ - fetchSeriesLabels: ( - timeRange: TimeRange, - name: string, - withName?: boolean, - withLimit?: string | number - ) => Promise>; - /** - * @deprecated Use queryLabelKeys() method instead. It'll determine the right endpoint based on the datasource settings - */ - fetchSeriesLabelsMatch: ( - timeRange: TimeRange, - name: string, - withLimit?: string | number - ) => Promise>; - /** - * @deprecated If you need labelKeys or labelValues please use queryLabelKeys() or queryLabelValues() functions - */ - fetchSeries: (timeRange: TimeRange, match: string) => Promise>>; - /** - * @deprecated If you need labelKeys or labelValues please use queryLabelKeys() or queryLabelValues() functions - */ - fetchDefaultSeries: (timeRange: TimeRange) => Promise<{}>; -} - -/** - * Old implementation of prometheus language provider. - * @deprecated Use PrometheusLanguageProviderInterface and PrometheusLanguageProvider class instead. - */ -export default class PromQlLanguageProvider extends LanguageProvider implements PrometheusLegacyLanguageProvider { - declare startTask: Promise; - declare labelFetchTs: number; - - datasource: PrometheusDatasource; - - histogramMetrics: string[]; - metrics: string[]; - metricsMetadata?: PromMetricsMetadata; - labelKeys: string[] = []; - - constructor(datasource: PrometheusDatasource, initialValues?: Partial) { - super(); - - this.datasource = datasource; - this.histogramMetrics = []; - this.metrics = []; - - Object.assign(this, initialValues); - } - - request = async (url: string, params = {}, options?: Partial) => { - try { - const res = await this.datasource.metadataRequest(url, params, options); - return res.data.data; - } catch (error) { - if (!isCancelledError(error)) { - console.error(error); - } - } - - return undefined; - }; - - /** - * Overridden by PrometheusLanguageProvider - */ - start = async (timeRange: TimeRange = getDefaultTimeRange()): Promise => { - if (this.datasource.lookupsDisabled) { - return []; - } - - this.metrics = (await this.fetchLabelValues(timeRange, '__name__')) || []; - this.histogramMetrics = processHistogramMetrics(this.metrics).sort(); - return Promise.all([this.loadMetricsMetadata(), this.fetchLabels(timeRange)]); - }; - - async loadMetricsMetadata() { - const secondsInDay = 86400; - const headers = buildCacheHeaders(getDaysToCacheMetadata(this.datasource.cacheLevel) * secondsInDay); - this.metricsMetadata = fixSummariesMetadata( - await this.request( - API_V1.METADATA, - {}, - { - showErrorAlert: false, - ...headers, - } - ) - ); - } - - getLabelKeys(): string[] { - return this.labelKeys; - } - - async getSeries(timeRange: TimeRange, selector: string, withName?: boolean): Promise> { - if (this.datasource.lookupsDisabled) { - return {}; - } - try { - if (selector === EMPTY_SELECTOR) { - return await this.fetchDefaultSeries(timeRange); - } else { - return await this.fetchSeriesLabels(timeRange, selector, withName, REMOVE_SERIES_LIMIT); - } - } catch (error) { - // TODO: better error handling - console.error(error); - return {}; - } - } - - fetchLabelValues = async (range: TimeRange, key: string, limit?: string | number): Promise => { - const params = { ...this.datasource.getAdjustedInterval(range), ...(limit ? { limit } : {}) }; - const interpolatedName = this.datasource.interpolateString(key); - const interpolatedAndEscapedName = escapeForUtf8Support(removeQuotesIfExist(interpolatedName)); - const value = await this.request( - API_V1.LABELS_VALUES(interpolatedAndEscapedName), - params, - getDefaultCacheHeaders(this.datasource.cacheLevel) - ); - return value ?? []; - }; - - async getLabelValues(range: TimeRange, key: string): Promise { - return await this.fetchLabelValues(range, key); - } - - /** - * Fetches all label keys - */ - fetchLabels = async (timeRange: TimeRange, queries?: PromQuery[], limit?: string): Promise => { - let url = API_V1.LABELS; - const timeParams = this.datasource.getAdjustedInterval(timeRange); - this.labelFetchTs = Date.now().valueOf(); - - const searchParams = new URLSearchParams({ ...timeParams, ...(limit ? { limit } : {}) }); - queries?.forEach((q) => { - const visualQuery = buildVisualQueryFromString(q.expr); - if (visualQuery.query.metric !== '') { - const isUtf8Metric = !isValidLegacyName(visualQuery.query.metric); - searchParams.append('match[]', isUtf8Metric ? `{"${visualQuery.query.metric}"}` : visualQuery.query.metric); - if (visualQuery.query.binaryQueries) { - visualQuery.query.binaryQueries.forEach((bq) => { - searchParams.append('match[]', isUtf8Metric ? `{"${bq.query.metric}"}` : bq.query.metric); - }); - } - } - }); - - if (this.datasource.httpMethod === 'GET') { - url += `?${searchParams.toString()}`; - } - - const res = await this.request(url, searchParams, getDefaultCacheHeaders(this.datasource.cacheLevel)); - if (Array.isArray(res)) { - this.labelKeys = res.slice().sort(); - return [...this.labelKeys]; - } - - return []; - }; - - /** - * Gets series values - * Function to replace old getSeries calls in a way that will provide faster endpoints - * for new prometheus instances, while maintaining backward compatability - */ - getSeriesValues = async (timeRange: TimeRange, labelName: string, selector: string): Promise => { - if (!this.datasource.hasLabelsMatchAPISupport()) { - const data = await this.getSeries(timeRange, selector); - return data[removeQuotesIfExist(labelName)] ?? []; - } - return await this.fetchSeriesValuesWithMatch(timeRange, labelName, selector); - }; - - /** - * Fetches all values for a label, with optional match[] - */ - fetchSeriesValuesWithMatch = async ( - timeRange: TimeRange, - name: string, - match?: string, - requestId?: string, - withLimit?: string | number - ): Promise => { - const interpolatedName = name ? this.datasource.interpolateString(name) : null; - const interpolatedMatch = match ? this.datasource.interpolateString(match) : null; - const range = this.datasource.getAdjustedInterval(timeRange); - const urlParams = { - ...range, - ...(interpolatedMatch && { 'match[]': interpolatedMatch }), - ...(withLimit ? { limit: withLimit } : {}), - }; - let requestOptions: Partial | undefined = { - ...getDefaultCacheHeaders(this.datasource.cacheLevel), - ...(requestId && { requestId }), - }; - - if (!Object.keys(requestOptions).length) { - requestOptions = undefined; - } - - const interpolatedAndEscapedName = escapeForUtf8Support(removeQuotesIfExist(interpolatedName ?? '')); - - const value = await this.request(API_V1.LABELS_VALUES(interpolatedAndEscapedName), urlParams, requestOptions); - return value ?? []; - }; - - /** - * Gets series labels - * Function to replace old getSeries calls in a way that will provide faster endpoints for new prometheus instances, - * while maintaining backward compatability. The old API call got the labels and the values in a single query, - * but with the new query we need two calls, one to get the labels, and another to get the values. - */ - getSeriesLabels = async (timeRange: TimeRange, selector: string, otherLabels: Label[]): Promise => { - let possibleLabelNames, data: Record; - - if (!this.datasource.hasLabelsMatchAPISupport()) { - data = await this.getSeries(timeRange, selector); - possibleLabelNames = Object.keys(data); // all names from prometheus - } else { - // Exclude __name__ from output - otherLabels.push({ name: '__name__', value: '', op: '!=' }); - data = await this.fetchSeriesLabelsMatch(timeRange, selector); - possibleLabelNames = Object.keys(data); - } - - const usedLabelNames = new Set(otherLabels.map((l) => l.name)); // names used in the query - return possibleLabelNames.filter((l) => !usedLabelNames.has(l)); - }; - - /** - * Fetch labels using the best endpoint that datasource supports. - * This is cached by its args but also by the global timeRange currently selected as they can change over requested time. - */ - fetchLabelsWithMatch = async ( - timeRange: TimeRange, - name: string, - withName?: boolean, - withLimit?: string | number - ): Promise> => { - if (this.datasource.hasLabelsMatchAPISupport()) { - return this.fetchSeriesLabelsMatch(timeRange, name, withLimit); - } else { - return this.fetchSeriesLabels(timeRange, name, withName, REMOVE_SERIES_LIMIT); - } - }; - - /** - * Fetch labels for a series using /series endpoint. This is cached by its args but also by the global timeRange currently selected as - * they can change over requested time. - */ - fetchSeriesLabels = async ( - timeRange: TimeRange, - name: string, - withName?: boolean, - withLimit?: string | number - ): Promise> => { - const interpolatedName = this.datasource.interpolateString(name); - const range = this.datasource.getAdjustedInterval(timeRange); - let urlParams = { - ...range, - 'match[]': interpolatedName, - ...(withLimit !== 'none' ? { limit: withLimit ?? DEFAULT_SERIES_LIMIT } : {}), - }; - - const data = await this.request(API_V1.SERIES, urlParams, getDefaultCacheHeaders(this.datasource.cacheLevel)); - const { values } = processLabels(data, withName); - return values; - }; - - /** - * Fetch labels for a series using /labels endpoint. This is cached by its args but also by the global timeRange currently selected as - * they can change over requested time. - */ - fetchSeriesLabelsMatch = async ( - timeRange: TimeRange, - name: string, - withLimit?: string | number - ): Promise> => { - const interpolatedName = this.datasource.interpolateString(name); - const range = this.datasource.getAdjustedInterval(timeRange); - const urlParams = { - ...range, - 'match[]': interpolatedName, - ...(withLimit ? { limit: withLimit } : {}), - }; - - const data: string[] = await this.request( - API_V1.LABELS, - urlParams, - getDefaultCacheHeaders(this.datasource.cacheLevel) - ); - // Convert string array to Record - return data.reduce((ac, a) => ({ ...ac, [a]: '' }), {}); - }; - - /** - * Fetch series for a selector. Use this for raw results. Use fetchSeriesLabels() to get labels. - */ - fetchSeries = async (timeRange: TimeRange, match: string): Promise>> => { - const range = this.datasource.getTimeRangeParams(timeRange); - const params = { ...range, 'match[]': match }; - return await this.request(API_V1.SERIES, params, getDefaultCacheHeaders(this.datasource.cacheLevel)); - }; - - /** - * Fetch this only one as we assume this won't change over time. This is cached differently from fetchSeriesLabels - * because we can cache more aggressively here and also we do not want to invalidate this cache the same way as in - * fetchSeriesLabels. - */ - fetchDefaultSeries = once(async (timeRange: TimeRange) => { - const values = await Promise.all(DEFAULT_KEYS.map((key) => this.fetchLabelValues(timeRange, key))); - return DEFAULT_KEYS.reduce((acc, key, i) => ({ ...acc, [key]: values[i] }), {}); - }); - - /** - * Fetch labels or values for a label based on the queries, scopes, filters and time range - */ - fetchSuggestions = async ( - timeRange?: TimeRange, - queries?: PromQuery[], - scopes?: Scope[], - adhocFilters?: AdHocVariableFilter[], - labelName?: string, - limit?: number, - requestId?: string - ): Promise => { - if (!timeRange) { - timeRange = getDefaultTimeRange(); - } - - const url = '/suggestions'; - const timeParams = this.datasource.getAdjustedInterval(timeRange); - const value = await this.request( - url, - { - labelName, - queries: queries?.map((q) => - this.datasource.interpolateString(q.expr, { - ...this.datasource.getIntervalVars(), - ...this.datasource.getRangeScopedVars(timeRange), - }) - ), - scopes: scopes?.reduce((acc, scope) => { - if (scope.spec.filters) { - acc.push(...scope.spec.filters); - } - - return acc; - }, []), - adhocFilters: adhocFilters?.map((filter) => ({ - key: filter.key, - operator: scopeFilterOperatorMap[filter.operator], - value: filter.value, - values: filter.values, - })), - limit, - ...timeParams, - }, - { - ...(requestId && { requestId }), - headers: { - ...getDefaultCacheHeaders(this.datasource.cacheLevel)?.headers, - 'Content-Type': 'application/json', - }, - method: 'POST', - } - ); - - return value ?? []; - }; -} - /** * Modern implementation of the Prometheus language provider that abstracts API endpoint selection. * @@ -531,9 +58,7 @@ export default class PromQlLanguageProvider extends LanguageProvider implements * @see LabelsApiClient For modern Prometheus versions using the labels API * @see SeriesApiClient For legacy Prometheus versions using the series API */ -export interface PrometheusLanguageProviderInterface - extends PrometheusBaseLanguageProvider, - PrometheusLegacyLanguageProvider { +export interface PrometheusLanguageProviderInterface extends PrometheusBaseLanguageProvider { /** * Initializes the language provider by fetching metrics, label keys, and metrics metadata using Resource Clients. * All calls use the limit parameter from datasource configuration (default: 40,000 if not set). @@ -541,7 +66,7 @@ export interface PrometheusLanguageProviderInterface * For backward compatibility, it calls _backwardCompatibleStart. * Some places still rely on deprecated fields. Until we replace them, we need _backwardCompatibleStart method. */ - start: (timeRange?: TimeRange) => Promise; + start: (timeRange?: TimeRange) => Promise; /** * Returns already cached metrics metadata including type and help information. @@ -591,14 +116,29 @@ export interface PrometheusLanguageProviderInterface queryLabelValues: (timeRange: TimeRange, labelKey: string, match?: string, limit?: number) => Promise; } -export class PrometheusLanguageProvider extends PromQlLanguageProvider implements PrometheusLanguageProviderInterface { +export class PrometheusLanguageProvider implements PrometheusLanguageProviderInterface { + public datasource: PrometheusDatasource; + private _metricsMetadata?: PromMetricsMetadata; private _resourceClient?: ResourceApiClient; constructor(datasource: PrometheusDatasource) { - super(datasource); + this.datasource = datasource; } + request = async (url: string, params = {}, options?: Partial) => { + try { + const res = await this.datasource.metadataRequest(url, params, options); + return res.data.data; + } catch (error) { + if (!isCancelledError(error)) { + console.error(error); + } + } + + return undefined; + }; + /** * Lazily initializes and returns the appropriate resource client based on Prometheus version. * @@ -624,24 +164,14 @@ export class PrometheusLanguageProvider extends PromQlLanguageProvider implement * Same start logic but it uses resource clients. Backward compatibility it calls _backwardCompatibleStart. * Some places still relies on deprecated fields. Until we replace them we need _backwardCompatibleStart method */ - start = async (timeRange: TimeRange = getDefaultTimeRange()): Promise => { + start = async (timeRange: TimeRange = getDefaultTimeRange()): Promise => { if (this.datasource.lookupsDisabled) { return []; } - await Promise.all([this.resourceClient.start(timeRange), this.queryMetricsMetadata(this.datasource.seriesLimit)]); - return this._backwardCompatibleStart(); - }; - - /** - * This private method exists to make sure the old class will be functional until we remove it. - * When we remove old class (PromQlLanguageProvider) we should remove this method too. - */ - private _backwardCompatibleStart = async () => { - this.metricsMetadata = this.retrieveMetricsMetadata(); - this.metrics = this.retrieveMetrics(); - this.histogramMetrics = this.retrieveHistogramMetrics(); - this.labelKeys = this.retrieveLabelKeys(); - return []; + return await Promise.all([ + this.resourceClient.start(timeRange), + this.queryMetricsMetadata(this.datasource.seriesLimit), + ]); }; /** @@ -654,7 +184,7 @@ export class PrometheusLanguageProvider extends PromQlLanguageProvider implement const secondsInDay = 86400; const headers = buildCacheHeaders(getDaysToCacheMetadata(this.datasource.cacheLevel) * secondsInDay); const metadata = await this.request( - API_V1.METADATA, + `/api/v1/metadata`, { limit: limit ?? this.datasource.seriesLimit }, { showErrorAlert: false, @@ -776,6 +306,63 @@ export class PrometheusLanguageProvider extends PromQlLanguageProvider implement limit ); }; + + /** + * Fetch labels or values for a label based on the queries, scopes, filters and time range + */ + fetchSuggestions = async ( + timeRange?: TimeRange, + queries?: PromQuery[], + scopes?: Scope[], + adhocFilters?: AdHocVariableFilter[], + labelName?: string, + limit?: number, + requestId?: string + ): Promise => { + if (!timeRange) { + timeRange = getDefaultTimeRange(); + } + + const url = '/suggestions'; + const timeParams = this.datasource.getAdjustedInterval(timeRange); + const value = await this.request( + url, + { + labelName, + queries: queries?.map((q) => + this.datasource.interpolateString(q.expr, { + ...this.datasource.getIntervalVars(), + ...this.datasource.getRangeScopedVars(timeRange), + }) + ), + scopes: scopes?.reduce((acc, scope) => { + if (scope.spec.filters) { + acc.push(...scope.spec.filters); + } + + return acc; + }, []), + adhocFilters: adhocFilters?.map((filter) => ({ + key: filter.key, + operator: scopeFilterOperatorMap[filter.operator], + value: filter.value, + values: filter.values, + })), + limit, + ...timeParams, + }, + { + ...(requestId && { requestId }), + headers: { + ...getDefaultCacheHeaders(this.datasource.cacheLevel)?.headers, + 'Content-Type': 'application/json', + }, + method: 'POST', + } + ); + + return value ?? []; + }; } export const importFromAbstractQuery = (labelBasedQuery: AbstractQuery): PromQuery => { diff --git a/packages/grafana-prometheus/src/language_utils.test.ts b/packages/grafana-prometheus/src/language_utils.test.ts index 555634f898e..6e69b7844d7 100644 --- a/packages/grafana-prometheus/src/language_utils.test.ts +++ b/packages/grafana-prometheus/src/language_utils.test.ts @@ -9,7 +9,6 @@ import { fixSummariesMetadata, getPrometheusTime, getRangeSnapInterval, - processLabels, removeQuotesIfExist, toPromLikeQuery, truncateResult, @@ -507,57 +506,6 @@ describe('truncateResult', () => { }); }); -describe('processLabels', () => { - it('export abstract query to expr', () => { - const labels: Array<{ [key: string]: string }> = [ - { label1: 'value1' }, - { label2: 'value2' }, - { label3: 'value3' }, - { label1: 'value1' }, - { label1: 'value1b' }, - ]; - - expect(processLabels(labels)).toEqual({ - keys: ['label1', 'label2', 'label3'], - values: { label1: ['value1', 'value1b'], label2: ['value2'], label3: ['value3'] }, - }); - }); - - it('dont wrap utf8 label values with quotes', () => { - const labels: Array<{ [key: string]: string }> = [ - { label1: 'value1' }, - { label2: 'value2' }, - { label3: 'value3 with space' }, - { label4: 'value4.with.dot' }, - ]; - - expect(processLabels(labels)).toEqual({ - keys: ['label1', 'label2', 'label3', 'label4'], - values: { - label1: ['value1'], - label2: ['value2'], - label3: [`value3 with space`], - label4: [`value4.with.dot`], - }, - }); - }); - - it('dont wrap utf8 labels with quotes', () => { - const labels: Array<{ [key: string]: string }> = [ - { 'label1 with space': 'value1' }, - { 'label2.with.dot': 'value2' }, - ]; - - expect(processLabels(labels)).toEqual({ - keys: ['label1 with space', 'label2.with.dot'], - values: { - 'label1 with space': ['value1'], - 'label2.with.dot': ['value2'], - }, - }); - }); -}); - describe('removeQuotesIfExist', () => { it('removes quotes from a string with double quotes', () => { const input = '"hello"'; diff --git a/packages/grafana-prometheus/src/language_utils.ts b/packages/grafana-prometheus/src/language_utils.ts index ee8090c9d44..38b5f364c42 100644 --- a/packages/grafana-prometheus/src/language_utils.ts +++ b/packages/grafana-prometheus/src/language_utils.ts @@ -15,7 +15,7 @@ import { import { addLabelToQuery } from './add_label_to_query'; import { getCacheDurationInMinutes } from './caching'; -import { SUGGESTIONS_LIMIT, PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './constants'; +import { PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './constants'; import { PrometheusCacheLevel, PromMetricsMetadata, PromMetricsMetadataItem, RecordingRuleIdentifier } from './types'; export const processHistogramMetrics = (metrics: string[]) => { @@ -31,38 +31,6 @@ export const processHistogramMetrics = (metrics: string[]) => { return [...resultSet]; }; -export function processLabels(labels: Array<{ [key: string]: string }>, withName = false) { - // For processing we are going to use sets as they have significantly better performance than arrays - // After we process labels, we will convert sets to arrays and return object with label values in arrays - const valueSet: { [key: string]: Set } = {}; - labels.forEach((label) => { - const { __name__, ...rest } = label; - if (withName) { - valueSet['__name__'] = valueSet['__name__'] || new Set(); - if (!valueSet['__name__'].has(__name__)) { - valueSet['__name__'].add(__name__); - } - } - - Object.keys(rest).forEach((key) => { - if (!valueSet[key]) { - valueSet[key] = new Set(); - } - if (!valueSet[key].has(rest[key])) { - valueSet[key].add(rest[key]); - } - }); - }); - - // valueArray that we are going to return in the object - const valueArray: { [key: string]: string[] } = {}; - limitSuggestions(Object.keys(valueSet)).forEach((key) => { - valueArray[key] = limitSuggestions(Array.from(valueSet[key])); - }); - - return { values: valueArray, keys: Object.keys(valueArray) }; -} - // This will capture 4 groups. Example label filter => {instance="10.4.11.4:9003"} // 1. label: instance // 2. operator: = @@ -276,10 +244,6 @@ function roundSecToNextMin(seconds: number, secondsToRound = 1): number { return Math.ceil(seconds / 60) - (Math.ceil(seconds / 60) % secondsToRound); } -function limitSuggestions(items: string[]) { - return items.slice(0, SUGGESTIONS_LIMIT); -} - const FromPromLikeMap: Record = { '=': AbstractLabelOperator.Equal, '!=': AbstractLabelOperator.NotEqual, diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index 038e62a70a2..140cd138539 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -12,6 +12,7 @@ export type { AnnotationTarget, AnnotationPanelFilter, VariableOption, + VariableRegexApplyTo, DashboardLink, DashboardLinkType, DashboardLinkPlacement, diff --git a/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts index 5f604cebfae..aedc041cf71 100644 --- a/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/newgauge/panelcfg/x/NewGaugePanelCfg_types.gen.ts @@ -15,6 +15,7 @@ export const pluginVersion = "12.4.0-pre"; export interface GaugePanelEffects { barGlow?: boolean; centerGlow?: boolean; + gradient?: boolean; rounded?: boolean; spotlight?: boolean; } @@ -22,6 +23,7 @@ export interface GaugePanelEffects { export const defaultGaugePanelEffects: Partial = { barGlow: false, centerGlow: false, + gradient: true, rounded: false, spotlight: false, }; @@ -29,7 +31,6 @@ export const defaultGaugePanelEffects: Partial = { export interface Options extends common.SingleStatBaseOptions { barWidthFactor: number; effects: GaugePanelEffects; - gradient: ('none' | 'auto'); segmentCount: number; segmentSpacing: number; shape: ('circle' | 'gauge'); @@ -41,7 +42,6 @@ export interface Options extends common.SingleStatBaseOptions { export const defaultOptions: Partial = { barWidthFactor: 0.5, effects: {}, - gradient: 'auto', segmentCount: 1, segmentSpacing: 0.3, shape: 'gauge', diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index c6a722cbae7..34aa9ac015b 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -187,6 +187,10 @@ export interface VariableModel { * Named capture groups can be used to separate the display text and value. */ regex?: string; + /** + * Determine whether regex applies to variable value or display text + */ + regexApplyTo?: VariableRegexApplyTo; /** * Whether the variable value should be managed by URL query params or not */ @@ -259,6 +263,12 @@ export enum VariableHide { inControlsMenu = 3, } +/** + * Determine whether regex applies to variable value or display text + * Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users) + */ +export type VariableRegexApplyTo = ('value' | 'text'); + /** * Sort variable options * Accepted values are: diff --git a/packages/grafana-schema/src/schema/dashboard/v2_examples.ts b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts index 7c542121a7b..e0f10d0f770 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2_examples.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2_examples.ts @@ -293,6 +293,7 @@ export const handyTestingSchema: Spec = { }, refresh: 'onDashboardLoad', regex: 'regex1', + regexApplyTo: 'value', skipUrlSync: false, sort: 'disabled', allowCustomValue: true, diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 67d24915bf1..78068b9412d 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -1105,6 +1105,7 @@ export interface QueryVariableSpec { datasource?: DataSourceRef; query: DataQueryKind; regex: string; + regexApplyTo?: VariableRegexApplyTo; sort: VariableSort; definition?: string; options: VariableOption[]; @@ -1125,6 +1126,7 @@ export const defaultQueryVariableSpec = (): QueryVariableSpec => ({ skipUrlSync: false, query: defaultDataQueryKind(), regex: "", + regexApplyTo: "value", sort: "disabled", options: [], multi: false, @@ -1161,6 +1163,12 @@ export type VariableRefresh = "never" | "onDashboardLoad" | "onTimeRangeChanged" export const defaultVariableRefresh = (): VariableRefresh => ("never"); +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +export type VariableRegexApplyTo = "value" | "text"; + +export const defaultVariableRegexApplyTo = (): VariableRegexApplyTo => ("value"); + // Sort variable options // Accepted values are: // `disabled`: No sorting diff --git a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts index 315fa4768a4..6a05bed3f1c 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts @@ -1111,6 +1111,7 @@ export interface QueryVariableSpec { description?: string; query: DataQueryKind; regex: string; + regexApplyTo?: VariableRegexApplyTo; sort: VariableSort; definition?: string; options: VariableOption[]; @@ -1131,6 +1132,7 @@ export const defaultQueryVariableSpec = (): QueryVariableSpec => ({ skipUrlSync: false, query: defaultDataQueryKind(), regex: "", + regexApplyTo: "value", sort: "disabled", options: [], multi: false, @@ -1167,6 +1169,12 @@ export type VariableRefresh = "never" | "onDashboardLoad" | "onTimeRangeChanged" export const defaultVariableRefresh = (): VariableRefresh => ("never"); +// Determine whether regex applies to variable value or display text +// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users) +export type VariableRegexApplyTo = "value" | "text"; + +export const defaultVariableRegexApplyTo = (): VariableRegexApplyTo => ("value"); + // Sort variable options // Accepted values are: // `disabled`: No sorting diff --git a/packages/grafana-ui/src/components/InteractiveTable/Expander/index.tsx b/packages/grafana-ui/src/components/InteractiveTable/Expander/index.tsx index 346d18b38fb..4aa0d0f634f 100644 --- a/packages/grafana-ui/src/components/InteractiveTable/Expander/index.tsx +++ b/packages/grafana-ui/src/components/InteractiveTable/Expander/index.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import { CellProps, HeaderProps } from 'react-table'; -import { t } from '@grafana/i18n'; +import { t, Trans } from '@grafana/i18n'; import { IconButton } from '../../IconButton/IconButton'; @@ -16,8 +16,9 @@ export function ExpanderCell({ row, __rowID }: CellProps ({ row, __rowID }: CellProps + Row expander + + ); +} + export function ExpanderHeader({ isAllRowsExpanded, toggleAllRowsExpanded }: HeaderProps) { return (
diff --git a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx index c8cf16cf2b6..e0df9d9782f 100644 --- a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx +++ b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx @@ -112,8 +112,6 @@ const meta: Meta> = { controls: { exclude: EXCLUDED_PROPS, }, - // TODO fix a11y issue in story and remove this - a11y: { test: 'off' }, }, args: { columns: [ diff --git a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.tsx b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.tsx index a046f24b89c..d5a25e2e480 100644 --- a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.tsx +++ b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.tsx @@ -14,6 +14,7 @@ import { } from 'react-table'; import { GrafanaTheme2, IconName, isTruthy } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { useStyles2 } from '../../themes/ThemeContext'; import { Icon } from '../Icon/Icon'; @@ -345,7 +346,7 @@ const getColumnHeaderStyles = (theme: GrafanaTheme2) => ({ }); function ColumnHeader({ - column: { canSort, render, isSorted, isSortedDesc, getSortByToggleProps }, + column: { canSort, render, isSorted, isSortedDesc, getSortByToggleProps, Header, id }, headerTooltip, }: { column: HeaderGroup; @@ -376,7 +377,13 @@ function ColumnHeader({ if (canSort) { return ( - ); diff --git a/packages/grafana-ui/src/components/InteractiveTable/utils.ts b/packages/grafana-ui/src/components/InteractiveTable/utils.ts index 68017bbfd19..2b664b16f6d 100644 --- a/packages/grafana-ui/src/components/InteractiveTable/utils.ts +++ b/packages/grafana-ui/src/components/InteractiveTable/utils.ts @@ -1,6 +1,6 @@ import { Column as RTColumn } from 'react-table'; -import { ExpanderCell, ExpanderHeader } from './Expander'; +import { EmptyExpanderHeader, ExpanderCell, ExpanderHeader } from './Expander'; import { Column } from './types'; export const EXPANDER_CELL_ID = '__expander' as const; @@ -18,9 +18,7 @@ export function getColumns( { id: EXPANDER_CELL_ID, Cell: ExpanderCell, - ...(showExpandAll && { - Header: ExpanderHeader, - }), + Header: showExpandAll ? ExpanderHeader : EmptyExpanderHeader, disableSortBy: true, width: 0, }, diff --git a/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx b/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx index ffba1eb88de..2960de1f502 100644 --- a/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx +++ b/packages/grafana-ui/src/components/Layout/Grid/Grid.tsx @@ -28,7 +28,7 @@ interface PropsWithMinColumnWidth extends GridPropsBase { /** For a responsive layout, fit as many columns while maintaining this minimum column width. * The real width will be calculated based on the theme spacing tokens: `theme.spacing(minColumnWidth)` */ - minColumnWidth?: ResponsiveProp<1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 44 | 55 | 72 | 89 | 144>; + minColumnWidth?: ResponsiveProp<1 | 2 | 3 | 5 | 8 | 13 | 16 | 21 | 34 | 44 | 55 | 72 | 89 | 144>; } /** 'columns' and 'minColumnWidth' are mutually exclusive */ diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx index 96e8b0f50f3..18147e0cac5 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialGauge.tsx @@ -2,7 +2,13 @@ import { css, cx } from '@emotion/css'; import { isNumber } from 'lodash'; import { useId } from 'react'; -import { DisplayValueAlignmentFactors, FieldDisplay, getDisplayProcessor, GrafanaTheme2 } from '@grafana/data'; +import { + DisplayValueAlignmentFactors, + FieldDisplay, + getDisplayProcessor, + GrafanaTheme2, + TimeRange, +} from '@grafana/data'; import { t } from '@grafana/i18n'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; @@ -66,6 +72,7 @@ export interface RadialGaugeProps { showScaleLabels?: boolean; /** For data links */ onClick?: React.MouseEventHandler; + timeRange?: TimeRange; } export type RadialGradientMode = 'none' | 'auto'; diff --git a/packages/grafana-ui/src/components/RadialGauge/effects.tsx b/packages/grafana-ui/src/components/RadialGauge/effects.tsx index 76f15900a14..551d9d91186 100644 --- a/packages/grafana-ui/src/components/RadialGauge/effects.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/effects.tsx @@ -8,7 +8,7 @@ export interface GlowGradientProps { } export function GlowGradient({ id, radius }: GlowGradientProps) { - const glowSize = 0.03 * radius; + const glowSize = 0.02 * radius; return ( diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.test.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.test.tsx index 9ad678132e9..91f3d9945f3 100644 --- a/packages/grafana-ui/src/components/Sparkline/Sparkline.test.tsx +++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.test.tsx @@ -27,4 +27,48 @@ describe('Sparkline', () => { render() ).not.toThrow(); }); + + it('should not throw an error if there is a single value', () => { + const sparkline: FieldSparkline = { + x: { + name: 'x', + values: [1679839200000], + type: FieldType.time, + config: {}, + }, + y: { + name: 'y', + values: [1], + type: FieldType.number, + config: {}, + state: { + range: { min: 1, max: 1, delta: 0 }, + }, + }, + }; + expect(() => + render() + ).not.toThrow(); + }); + + it('should not throw an error if there are no values', () => { + const sparkline: FieldSparkline = { + x: { + name: 'x', + values: [], + type: FieldType.time, + config: {}, + }, + y: { + name: 'y', + values: [], + type: FieldType.number, + config: {}, + state: {}, + }, + }; + expect(() => + render() + ).not.toThrow(); + }); }); diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx index 8de8ed1b893..68b1a5a832f 100644 --- a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx +++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx @@ -1,32 +1,13 @@ -import { isEqual } from 'lodash'; -import { PureComponent } from 'react'; -import { AlignedData, Range } from 'uplot'; +import React, { memo } from 'react'; -import { - compareDataFrameStructures, - DataFrame, - Field, - FieldConfig, - FieldSparkline, - FieldType, - getFieldColorModeForField, - nullToValue, -} from '@grafana/data'; -import { - AxisPlacement, - GraphDrawStyle, - GraphFieldConfig, - VisibilityMode, - ScaleDirection, - ScaleOrientation, -} from '@grafana/schema'; +import { FieldConfig, FieldSparkline } from '@grafana/data'; +import { GraphFieldConfig } from '@grafana/schema'; import { Themeable2 } from '../../types/theme'; import { UPlotChart } from '../uPlot/Plot'; -import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; import { preparePlotData2, getStackingGroups } from '../uPlot/utils'; -import { getYRange, preparePlotFrame } from './utils'; +import { prepareSeries, prepareConfig } from './utils'; export interface SparklineProps extends Themeable2 { width: number; @@ -35,169 +16,18 @@ export interface SparklineProps extends Themeable2 { sparkline: FieldSparkline; } -interface State { - data: AlignedData; - alignedDataFrame: DataFrame; - configBuilder: UPlotConfigBuilder; -} +export const Sparkline: React.FC = memo((props) => { + const { sparkline, config: fieldConfig, theme, width, height } = props; -const defaultConfig: GraphFieldConfig = { - drawStyle: GraphDrawStyle.Line, - showPoints: VisibilityMode.Auto, - axisPlacement: AxisPlacement.Hidden, - pointSize: 2, -}; - -/** @internal */ -export class Sparkline extends PureComponent { - constructor(props: SparklineProps) { - super(props); - - const alignedDataFrame = preparePlotFrame(props.sparkline, props.config); - - this.state = { - data: preparePlotData2(alignedDataFrame, getStackingGroups(alignedDataFrame)), - alignedDataFrame, - configBuilder: this.prepareConfig(alignedDataFrame), - }; + const { frame: alignedDataFrame, warning } = prepareSeries(sparkline, fieldConfig); + if (warning) { + return null; } - static getDerivedStateFromProps(props: SparklineProps, state: State) { - const _frame = preparePlotFrame(props.sparkline, props.config); - const frame = nullToValue(_frame); - if (!frame) { - return { ...state }; - } + const data = preparePlotData2(alignedDataFrame, getStackingGroups(alignedDataFrame)); + const configBuilder = prepareConfig(sparkline, alignedDataFrame, theme); - return { - ...state, - data: preparePlotData2(frame, getStackingGroups(frame)), - alignedDataFrame: frame, - }; - } + return ; +}); - componentDidUpdate(prevProps: SparklineProps, prevState: State) { - const { alignedDataFrame } = this.state; - - if (!alignedDataFrame) { - return; - } - - let rebuildConfig = false; - - if (prevProps.sparkline !== this.props.sparkline) { - const isStructureChanged = !compareDataFrameStructures(this.state.alignedDataFrame, prevState.alignedDataFrame); - const isRangeChanged = !isEqual( - alignedDataFrame.fields[1].state?.range, - prevState.alignedDataFrame.fields[1].state?.range - ); - rebuildConfig = isStructureChanged || isRangeChanged; - } else { - rebuildConfig = !isEqual(prevProps.config, this.props.config); - } - - if (rebuildConfig) { - this.setState({ configBuilder: this.prepareConfig(alignedDataFrame) }); - } - } - - getYRange(field: Field): Range.MinMax { - return getYRange(field, this.state.alignedDataFrame); - } - - prepareConfig(data: DataFrame) { - const { theme } = this.props; - const builder = new UPlotConfigBuilder(); - - builder.setCursor({ - show: false, - x: false, // no crosshairs - y: false, - }); - - // X is the first field in the alligned frame - const xField = data.fields[0]; - builder.addScale({ - scaleKey: 'x', - orientation: ScaleOrientation.Horizontal, - direction: ScaleDirection.Right, - isTime: false, //xField.type === FieldType.time, - range: () => { - const { sparkline } = this.props; - if (sparkline.x) { - if (sparkline.timeRange && sparkline.x.type === FieldType.time) { - return [sparkline.timeRange.from.valueOf(), sparkline.timeRange.to.valueOf()]; - } - const vals = sparkline.x.values; - return [vals[0], vals[vals.length - 1]]; - } - return [0, sparkline.y.values.length - 1]; - }, - }); - - builder.addAxis({ - scaleKey: 'x', - theme, - placement: AxisPlacement.Hidden, - }); - - for (let i = 0; i < data.fields.length; i++) { - const field = data.fields[i]; - const config: FieldConfig = field.config; - const customConfig: GraphFieldConfig = { - ...defaultConfig, - ...config.custom, - }; - - if (field === xField || field.type !== FieldType.number) { - continue; - } - - const scaleKey = config.unit || '__fixed'; - builder.addScale({ - scaleKey, - orientation: ScaleOrientation.Vertical, - direction: ScaleDirection.Up, - range: () => this.getYRange(field), - }); - - builder.addAxis({ - scaleKey, - theme, - placement: AxisPlacement.Hidden, - }); - - const colorMode = getFieldColorModeForField(field); - const seriesColor = colorMode.getCalculator(field, theme)(0, 0); - const pointsMode = - customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints; - - builder.addSeries({ - pxAlign: false, - scaleKey, - theme, - colorMode, - thresholds: config.thresholds, - drawStyle: customConfig.drawStyle!, - lineColor: customConfig.lineColor ?? seriesColor, - lineWidth: customConfig.lineWidth, - lineInterpolation: customConfig.lineInterpolation, - showPoints: pointsMode, - pointSize: customConfig.pointSize, - fillOpacity: customConfig.fillOpacity, - fillColor: customConfig.fillColor, - lineStyle: customConfig.lineStyle, - gradientMode: customConfig.gradientMode, - spanNulls: customConfig.spanNulls, - }); - } - - return builder; - } - - render() { - const { data, configBuilder } = this.state; - const { width, height } = this.props; - return ; - } -} +Sparkline.displayName = 'Sparkline'; diff --git a/packages/grafana-ui/src/components/Sparkline/utils.test.ts b/packages/grafana-ui/src/components/Sparkline/utils.test.ts index 9eb914af438..2a77677d998 100644 --- a/packages/grafana-ui/src/components/Sparkline/utils.test.ts +++ b/packages/grafana-ui/src/components/Sparkline/utils.test.ts @@ -114,6 +114,13 @@ describe('Get y range', () => { config: {}, state: { range: { min: -2, max: -2, delta: 0 } }, }; + const decimalsCloseYField: Field = { + name: 'y', + values: [2, 1.999999999999999, 2.000000000000001, 2, 2], + type: FieldType.number, + config: {}, + state: { range: { min: 1.999999999999999, max: 2.000000000000001, delta: 0 } }, + }; const xField: Field = { name: 'x', values: [1000, 2000, 3000, 4000, 5000], @@ -154,12 +161,12 @@ describe('Get y range', () => { { description: 'straight line', field: straightLineYField, - expected: [0, 4], + expected: [2, 4], }, { description: 'straight line, negative values', field: straightLineNegYField, - expected: [-4, 0], + expected: [-4, -2], }, { description: 'straight line with config min and max', @@ -171,8 +178,13 @@ describe('Get y range', () => { field: { ...straightLineYField, config: { noValue: '0' } }, expected: [0, 2], }, + { + description: 'long decimals which are nearly equal and result in a functional delta of 0', + field: decimalsCloseYField, + expected: [2, 4], + }, ])(`should return correct range for $description`, ({ field, expected }) => { - const actual = getYRange(field, getAlignedFrame(field)); + const actual = getYRange(getAlignedFrame(field)); expect(actual).toEqual(expected); expect(actual[0]).toBeLessThan(actual[1]!); }); diff --git a/packages/grafana-ui/src/components/Sparkline/utils.ts b/packages/grafana-ui/src/components/Sparkline/utils.ts index 5c2afd8a1cb..e4d17a85c15 100644 --- a/packages/grafana-ui/src/components/Sparkline/utils.ts +++ b/packages/grafana-ui/src/components/Sparkline/utils.ts @@ -1,16 +1,29 @@ import { Range } from 'uplot'; import { + applyNullInsertThreshold, DataFrame, FieldConfig, FieldSparkline, FieldType, + getFieldColorModeForField, + GrafanaTheme2, isLikelyAscendingVector, + nullToValue, + roundDecimals, sortDataFrame, - applyNullInsertThreshold, - Field, } from '@grafana/data'; -import { GraphFieldConfig } from '@grafana/schema'; +import { t } from '@grafana/i18n'; +import { + AxisPlacement, + GraphDrawStyle, + GraphFieldConfig, + VisibilityMode, + ScaleDirection, + ScaleOrientation, +} from '@grafana/schema'; + +import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; /** @internal * Given a sparkline config returns a DataFrame ready to be turned into Plot data set @@ -55,33 +68,176 @@ export function preparePlotFrame(sparkline: FieldSparkline, config?: FieldConfig /** * apply configuration defaults and ensure that the range is never two equal values. */ -export function getYRange(field: Field, alignedFrame: DataFrame): Range.MinMax { - let { min, max } = alignedFrame.fields[1].state?.range!; +export function getYRange(alignedFrame: DataFrame): Range.MinMax { + const field = alignedFrame.fields[1]; + let { min, max } = field.state?.range!; - // enure that the min/max from the field config are respected - min = Math.max(min!, field.config.min ?? -Infinity); - max = Math.min(max!, field.config.max ?? Infinity); + // enure that the min/max from the field config are respected. + min = Math.min(min!, field.config.min ?? Infinity); + max = Math.max(max!, field.config.max ?? -Infinity); + + // console.log({ min, max }); // if noValue is set, ensure that it is included in the range as well - const noValue = +alignedFrame.fields[1].config?.noValue!; + const noValue = +field.config?.noValue!; if (!Number.isNaN(noValue)) { min = Math.min(min, noValue); max = Math.max(max, noValue); } - // if min and max are equal after all of that, create a range - // that allows the sparkline to be visible in the center of the viz - if (min === max) { - if (min === 0) { - max = 100; - } else if (min < 0) { - max = 0; - min *= 2; - } else { - min = 0; - max *= 2; - } + // call roundDecimals to mirror what is going to eventually happen in uplot + let roundedMin = roundDecimals(min, field.config.decimals ?? 0); + let roundedMax = roundDecimals(max, field.config.decimals ?? 0); + + // if the rounded min and max are different, + // we can return the real min and max. + if (roundedMin !== roundedMax) { + return [min, max]; } - return [min, max]; + // we are forced to tweak the min and max since they + // will be treated as equal after rounding by uPlot. + if (roundedMin === 0) { + // both are zero + roundedMax = 1; + } else if (roundedMin < 0) { + // both are negative + // max = 0; + roundedMin *= 2; + } else { + // both are positive + // min = 0; + roundedMax *= 2; + } + + return [roundedMin, roundedMax]; } + +// TODO: #112977 enable highlight index +// const HIGHLIGHT_IDX_POINT_SIZE = 6; + +const defaultConfig: GraphFieldConfig = { + drawStyle: GraphDrawStyle.Line, + showPoints: VisibilityMode.Auto, + axisPlacement: AxisPlacement.Hidden, + pointSize: 2, +}; + +export const prepareSeries = ( + sparkline: FieldSparkline, + fieldConfig?: FieldConfig +): { frame: DataFrame; warning?: string } => { + const frame = nullToValue(preparePlotFrame(sparkline, fieldConfig)); + if (frame.fields.some((f) => f.values.length <= 1)) { + return { + warning: t( + 'grafana-ui.components.sparkline.warning.too-few-values', + 'Sparkline requires at least two values to render.' + ), + frame, + }; + } + return { frame }; +}; + +export const prepareConfig = ( + sparkline: FieldSparkline, + dataFrame: DataFrame, + theme: GrafanaTheme2 +): UPlotConfigBuilder => { + const builder = new UPlotConfigBuilder(); + // const rangePad = HIGHLIGHT_IDX_POINT_SIZE / 2; + + builder.setCursor({ + show: false, + x: false, // no crosshairs + y: false, + }); + + // X is the first field in the aligned frame + const xField = dataFrame.fields[0]; + builder.addScale({ + scaleKey: 'x', + orientation: ScaleOrientation.Horizontal, + direction: ScaleDirection.Right, + isTime: false, // xField.type === FieldType.time, + range: () => { + if (sparkline.x) { + if (sparkline.timeRange && sparkline.x.type === FieldType.time) { + return [sparkline.timeRange.from.valueOf(), sparkline.timeRange.to.valueOf()]; + } + const vals = sparkline.x.values; + return [vals[0], vals[vals.length - 1]]; + } + return [0, sparkline.y.values.length - 1]; + }, + }); + + builder.addAxis({ + scaleKey: 'x', + theme, + placement: AxisPlacement.Hidden, + }); + + for (let i = 0; i < dataFrame.fields.length; i++) { + const field = dataFrame.fields[i]; + const config: FieldConfig = field.config; + const customConfig: GraphFieldConfig = { + ...defaultConfig, + ...config.custom, + }; + + if (field === xField || field.type !== FieldType.number) { + continue; + } + + const scaleKey = config.unit || '__fixed'; + builder.addScale({ + scaleKey, + orientation: ScaleOrientation.Vertical, + direction: ScaleDirection.Up, + range: () => getYRange(dataFrame), + }); + + builder.addAxis({ + scaleKey, + theme, + placement: AxisPlacement.Hidden, + }); + + const colorMode = getFieldColorModeForField(field); + const seriesColor = colorMode.getCalculator(field, theme)(0, 0); + // TODO: #112977 enable highlight index and adjust padding accordingly + // const hasHighlightIndex = typeof sparkline.highlightIndex === 'number'; + // if (hasHighlightIndex) { + // builder.setPadding([rangePad, rangePad, rangePad, rangePad]); + // } + const pointsMode = + customConfig.drawStyle === GraphDrawStyle.Points // || hasHighlightIndex + ? VisibilityMode.Always + : customConfig.showPoints; + + builder.addSeries({ + pxAlign: false, + scaleKey, + theme, + colorMode, + thresholds: config.thresholds, + drawStyle: customConfig.drawStyle!, + lineColor: customConfig.lineColor ?? seriesColor, + lineWidth: customConfig.lineWidth, + lineInterpolation: customConfig.lineInterpolation, + showPoints: pointsMode, + // TODO: #112977 enable highlight index + pointSize: /* hasHighlightIndex ? HIGHLIGHT_IDX_POINT_SIZE : */ customConfig.pointSize, + // pointsFilter: hasHighlightIndex ? [sparkline.highlightIndex!] : undefined, + fillOpacity: customConfig.fillOpacity, + fillColor: customConfig.fillColor, + lineStyle: customConfig.lineStyle, + gradientMode: customConfig.gradientMode, + spanNulls: customConfig.spanNulls, + }); + } + + return builder; +}; diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx index cba6f984501..b9af4d23fc4 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx @@ -16,10 +16,6 @@ const meta: Meta = { containerWidth: '100%', seriesCount: 5, }, - parameters: { - // TODO fix a11y issue in story and remove this - a11y: { test: 'off' }, - }, argTypes: { containerWidth: { control: { diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx index b0f578fae79..b654a2d3ac6 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx @@ -29,11 +29,9 @@ export const VizLegendTable = ({ isSortable, }: VizLegendTableProps): JSX.Element => { const styles = useStyles2(getStyles); - const header: Record = {}; - - if (isSortable) { - header[nameSortKey] = ''; - } + const header: Record = { + [nameSortKey]: '', + }; for (const item of items) { if (item.getDisplayValues) { @@ -90,16 +88,18 @@ export const VizLegendTable = ({ - {!isSortable && } {Object.keys(header).map((columnTitle) => (
{ - if (onToggleSort) { + if (onToggleSort && isSortable) { onToggleSort(columnTitle); } }} diff --git a/pkg/api/apierrors/folder.go b/pkg/api/apierrors/folder.go index 81b16c3899e..9509ff4ff55 100644 --- a/pkg/api/apierrors/folder.go +++ b/pkg/api/apierrors/folder.go @@ -57,7 +57,11 @@ func ToFolderErrorResponse(err error) response.Response { // --- Kubernetes status errors --- var statusErr *k8sErrors.StatusError if errors.As(err, &statusErr) { - return response.Error(int(statusErr.ErrStatus.Code), statusErr.ErrStatus.Message, err) + message := statusErr.ErrStatus.Message + if message == "" { + message = getDefaultMessageForStatus(int(statusErr.ErrStatus.Code)) + } + return response.Error(int(statusErr.ErrStatus.Code), message, err) } return response.ErrOrFallback(http.StatusInternalServerError, fmt.Sprintf("Folder API error: %s", err.Error()), err) @@ -100,6 +104,19 @@ func ToFolderStatusError(err error) k8sErrors.StatusError { } } +func getDefaultMessageForStatus(statusCode int) string { + switch statusCode { + case http.StatusForbidden: + return "Access denied" + case http.StatusNotFound: + return "Folder not found" + case http.StatusBadRequest: + return "Invalid request" + default: + return "Folder API error" + } +} + func IsForbidden(err error) bool { return k8sErrors.IsForbidden(err) || errors.Is(err, dashboards.ErrFolderAccessDenied) } diff --git a/pkg/api/apierrors/folder_test.go b/pkg/api/apierrors/folder_test.go index 2def7fb48a4..0ca8b16fc87 100644 --- a/pkg/api/apierrors/folder_test.go +++ b/pkg/api/apierrors/folder_test.go @@ -125,7 +125,7 @@ func TestToFolderErrorResponse(t *testing.T) { }, // --- Kubernetes status errors --- { - name: "kubernetes status error", + name: "kubernetes status error with message", input: &k8sErrors.StatusError{ ErrStatus: metav1.Status{ Code: 412, @@ -139,6 +139,66 @@ func TestToFolderErrorResponse(t *testing.T) { }, }), }, + { + name: "kubernetes status error with empty message - 403 forbidden", + input: &k8sErrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusForbidden, + Message: "", + }, + }, + want: response.Error(http.StatusForbidden, "Access denied", &k8sErrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusForbidden, + Message: "", + }, + }), + }, + { + name: "kubernetes status error with empty message - 404 not found", + input: &k8sErrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Message: "", + }, + }, + want: response.Error(http.StatusNotFound, "Folder not found", &k8sErrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Message: "", + }, + }), + }, + { + name: "kubernetes status error with empty message - 400 bad request", + input: &k8sErrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusBadRequest, + Message: "", + }, + }, + want: response.Error(http.StatusBadRequest, "Invalid request", &k8sErrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusBadRequest, + Message: "", + }, + }), + }, + { + name: "kubernetes status error with empty message - default fallback", + input: &k8sErrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusInternalServerError, + Message: "", + }, + }, + want: response.Error(http.StatusInternalServerError, "Folder API error", &k8sErrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusInternalServerError, + Message: "", + }, + }), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index 984a9b24831..4362c048b0a 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -161,6 +161,7 @@ var serviceIdentityTokenPermissions = []string{ "preferences.grafana.app:*", // user, team, and org preferences "collections.grafana.app:*", // user stars "plugins.grafana.app:*", + "historian.alerting.grafana.app:*", // Secrets Manager uses a custom verb for secret decryption, and its authorizer does not allow wildcard permissions. "secret.grafana.app/securevalues:decrypt", diff --git a/pkg/kinds/dashboard/dashboard_spec_gen.go b/pkg/kinds/dashboard/dashboard_spec_gen.go index fd8dbae7b77..bd73e527534 100644 --- a/pkg/kinds/dashboard/dashboard_spec_gen.go +++ b/pkg/kinds/dashboard/dashboard_spec_gen.go @@ -834,6 +834,8 @@ type VariableModel struct { // Optional field, if you want to extract part of a series name or metric node segment. // Named capture groups can be used to separate the display text and value. Regex *string `json:"regex,omitempty"` + // Determine whether regex applies to variable value or display text + RegexApplyTo *VariableRegexApplyTo `json:"regexApplyTo,omitempty"` // Additional static options for query variable StaticOptions []VariableOption `json:"staticOptions,omitempty"` // Ordering of static options in relation to options returned from data source for query variable @@ -942,6 +944,15 @@ const ( VariableSortNaturalDesc VariableSort = 8 ) +// Determine whether regex applies to variable value or display text +// Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users) +type VariableRegexApplyTo string + +const ( + VariableRegexApplyToValue VariableRegexApplyTo = "value" + VariableRegexApplyToText VariableRegexApplyTo = "text" +) + // Contains the list of annotations that are associated with the dashboard. // Annotations are used to overlay event markers and overlay event tags on graphs. // Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API. diff --git a/pkg/registry/apis/dashboard/legacy/client.go b/pkg/registry/apis/dashboard/legacy/client.go index 913dd02d14d..258d0d1a519 100644 --- a/pkg/registry/apis/dashboard/legacy/client.go +++ b/pkg/registry/apis/dashboard/legacy/client.go @@ -95,3 +95,7 @@ func (d *directResourceClient) BulkProcess(ctx context.Context, opts ...grpc.Cal func (b *directResourceClient) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) { return nil, fmt.Errorf("not implemented") } + +func (b *directResourceClient) GetQuotaUsage(ctx context.Context, req *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/pkg/registry/apis/dashboard/legacy/queries_test.go b/pkg/registry/apis/dashboard/legacy/queries_test.go index 1345bfc2925..ec865873960 100644 --- a/pkg/registry/apis/dashboard/legacy/queries_test.go +++ b/pkg/registry/apis/dashboard/legacy/queries_test.go @@ -100,6 +100,42 @@ func TestDashboardQueries(t *testing.T) { Order: "ASC", }), }, + { + // Tests that MaxRows generates LIMIT clause for regular dashboard queries + Name: "dashboard_with_max_rows", + Data: getQuery(&DashboardQuery{ + OrgID: 2, + MaxRows: 100, + }), + }, + { + // Tests that MaxRows generates LIMIT clause for history queries + Name: "history_with_max_rows", + Data: getQuery(&DashboardQuery{ + OrgID: 1, + GetHistory: true, + MaxRows: 50, + }), + }, + { + // Tests that MaxRows + LastID generates correct pagination query + Name: "dashboard_with_max_rows_last_id", + Data: getQuery(&DashboardQuery{ + OrgID: 2, + MaxRows: 100, + LastID: 500, + }), + }, + { + // Tests that MaxRows + LastID generates correct pagination query + Name: "history_with_max_rows_last_id", + Data: getQuery(&DashboardQuery{ + OrgID: 2, + MaxRows: 100, + GetHistory: true, + LastID: 500, + }), + }, }, sqlQueryPanels: { { diff --git a/pkg/registry/apis/dashboard/legacy/query_dashboards.sql b/pkg/registry/apis/dashboard/legacy/query_dashboards.sql index e8503170a2c..b32c5aa3465 100644 --- a/pkg/registry/apis/dashboard/legacy/query_dashboards.sql +++ b/pkg/registry/apis/dashboard/legacy/query_dashboards.sql @@ -71,6 +71,9 @@ WHERE dashboard.is_folder = {{ .Arg .Query.GetFolders }} COALESCE(dashboard_version.version, dashboard.version) {{ .Query.Order }}, {{ end }} dashboard.uid ASC + {{ if .Query.MaxRows }} + LIMIT {{ .Arg .Query.MaxRows }} + {{ end }} {{ else }} {{ if .Query.UID }} AND dashboard.uid = {{ .Arg .Query.UID }} @@ -83,4 +86,7 @@ WHERE dashboard.is_folder = {{ .Arg .Query.GetFolders }} AND dashboard.deleted IS NULL {{ end }} ORDER BY dashboard.id DESC + {{ if .Query.MaxRows }} + LIMIT {{ .Arg .Query.MaxRows }} + {{ end }} {{ end }} diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 24ec135e785..077cf0a6700 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -140,11 +140,30 @@ func NewDashboardSQLAccess(sql legacysql.LegacyDatabaseProvider, } func (a *dashboardSqlAccess) executeQuery(ctx context.Context, helper *legacysql.LegacyDatabaseHelper, query string, args ...any) (*sql.Rows, error) { - // Use transaction if available in context. + var tx *sql.Tx + // After this function runs, the `tx` variable will only be set if + // this function was called in the context of a transaction set up by a + // caller upstream. In that case, we reuse the transaction. + _ = helper.DB.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + coreTx, err := sess.Tx() + if err != nil { + return nil + } + + tx = coreTx.Tx + return nil + }) + + // Use transaction from unified storage if available in the context. // This allows us to run migrations in a transaction which is specifically required for SQLite. - if tx := resource.TransactionFromContext(ctx); tx != nil { + if tx == nil { + tx = resource.TransactionFromContext(ctx) + } + + if tx != nil { return tx.QueryContext(ctx, query, args...) } + return helper.DB.GetSqlxSession().Query(ctx, query, args...) } @@ -693,6 +712,128 @@ func (r *rowsWrapper) Value() []byte { return b } +// batchingIterator wraps rowsWrapper to fetch data in batches +type batchingIterator struct { + wrapper *rowsWrapper + a *dashboardSqlAccess + ctx context.Context + helper *legacysql.LegacyDatabaseHelper + query *DashboardQuery + batchSize int + done bool + err error +} + +var _ resource.ListIterator = (*batchingIterator)(nil) + +func (b *batchingIterator) Error() error { + if b.err != nil { + return b.err + } + return b.wrapper.Error() +} + +func (b *batchingIterator) ContinueToken() string { + return b.wrapper.ContinueToken() +} + +func (b *batchingIterator) ResourceVersion() int64 { + return b.wrapper.ResourceVersion() +} + +func (b *batchingIterator) Namespace() string { + return b.wrapper.Namespace() +} + +func (b *batchingIterator) Name() string { + return b.wrapper.Name() +} + +func (b *batchingIterator) Folder() string { + return b.wrapper.Folder() +} + +func (b *batchingIterator) Value() []byte { + return b.wrapper.Value() +} + +func (b *batchingIterator) Close() error { + return b.wrapper.Close() +} + +func newBatchingIterator(ctx context.Context, a *dashboardSqlAccess, helper *legacysql.LegacyDatabaseHelper, query *DashboardQuery) (*batchingIterator, error) { + iter := &batchingIterator{ + a: a, + ctx: ctx, + helper: helper, + query: query, + batchSize: query.MaxRows, + } + + // Loads the first batch + if err := iter.nextBatch(query.LastID); err != nil { + return nil, err + } + return iter, nil +} + +func (b *batchingIterator) nextBatch(lastID int64) error { + b.query.LastID = lastID + wrapper, err := b.a.getRows(b.ctx, b.helper, b.query) + if err != nil { + return err + } + b.wrapper = wrapper + return nil +} + +func (b *batchingIterator) Next() bool { + if b.done { + return false + } + + // Try to get next row from current batch + if b.wrapper.Next() { + return true + } + + // Check for errors in current wrapper + if b.Error() != nil { + return false + } + + // No more rows in current batch - close it + if err := b.wrapper.Close(); err != nil { + // Should not happen, but handle it + b.err = err + b.done = true + return false + } + + // Current batch exhausted - check if we got a full batch (might be more data) + if b.wrapper.count < b.batchSize { + // Got fewer rows than batch size, so we're done + b.done = true + return false + } + + // Fetch next batch with LastID from last row + if err := b.nextBatch(b.wrapper.row.token.id); err != nil { + b.err = err + b.done = true + return false + } + + // Try to get first row from new batch + if b.wrapper.Next() { + return true + } + + // New batch is empty, we're done + b.done = true + return false +} + func generateFallbackDashboard(data []byte, title, uid string) ([]byte, error) { generatedDashboard := map[string]interface{}{ "editable": true, diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index d8c948c9664..3d4eb6dcd91 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -132,10 +132,19 @@ func (a *dashboardSqlAccess) WriteEvent(ctx context.Context, event resource.Writ } } else { failOnExisting := event.Type == resourcepb.WatchEvent_ADDED - after, _, err := a.SaveDashboard(ctx, info.OrgID, dash, failOnExisting) + sql, err := a.sql(ctx) if err != nil { return 0, err } + + var after *dashboard.Dashboard + if err := sql.DB.InTransaction(ctx, func(ctx context.Context) error { + var err error + after, _, err = a.SaveDashboard(ctx, info.OrgID, dash, failOnExisting) + return err + }); err != nil { + return 0, err + } if after != nil { meta, err := utils.MetaAccessor(after) if err != nil { @@ -296,11 +305,19 @@ func (a *dashboardSqlAccess) ListIterator(ctx context.Context, req *resourcepb.L return 0, fmt.Errorf("token and orgID mismatch") } + // Default batch size for iterator - fetch rows in batches to avoid slow queries + const defaultMaxRows = 500 + maxRows := defaultMaxRows + if req.Limit > 0 && req.Limit < int64(defaultMaxRows) { + maxRows = int(req.Limit) + } + query := &DashboardQuery{ - OrgID: info.OrgID, - Limit: int(req.Limit), - LastID: token.id, - Labels: req.Options.Labels, + OrgID: info.OrgID, + Limit: int(req.Limit), + MaxRows: maxRows, + LastID: token.id, + Labels: req.Options.Labels, } sql, err := a.sql(ctx) @@ -323,14 +340,15 @@ func (a *dashboardSqlAccess) ListIterator(ctx context.Context, req *resourcepb.L return 0, err } listRV *= 1000 // Convert to microseconds - rows, err := a.getRows(ctx, sql, query) - if rows != nil { + + iter, err := newBatchingIterator(ctx, a, sql, query) + if iter != nil { defer func() { - _ = rows.Close() + _ = iter.Close() }() } if err == nil { - err = cb(rows) + err = cb(iter) } return listRV, err } diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_with_max_rows.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_with_max_rows.sql new file mode 100755 index 00000000000..2470760c9e3 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_with_max_rows.sql @@ -0,0 +1,31 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard.updated, + updated_user.uid as updated_by, + dashboard.updated_by as updated_by_id, + dashboard.version, + '' as message, + dashboard.data, + dashboard.api_version +FROM `grafana`.`dashboard` as dashboard +LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard.deleted IS NULL + ORDER BY dashboard.id DESC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_with_max_rows_last_id.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_with_max_rows_last_id.sql new file mode 100755 index 00000000000..d6c5ce10313 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-dashboard_with_max_rows_last_id.sql @@ -0,0 +1,32 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard.updated, + updated_user.uid as updated_by, + dashboard.updated_by as updated_by_id, + dashboard.version, + '' as message, + dashboard.data, + dashboard.api_version +FROM `grafana`.`dashboard` as dashboard +LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard.id < 500 + AND dashboard.deleted IS NULL + ORDER BY dashboard.id DESC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_with_max_rows.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_with_max_rows.sql new file mode 100755 index 00000000000..96efe4956a9 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_with_max_rows.sql @@ -0,0 +1,34 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard_version.created as updated, + updated_user.uid as updated_by, + dashboard_version.created_by as updated_by_id, + dashboard_version.version, + dashboard_version.message, + dashboard_version.data, + dashboard_version.api_version +FROM `grafana`.`dashboard` as dashboard +LEFT OUTER JOIN `grafana`.`dashboard_version` as dashboard_version ON dashboard.id = dashboard_version.dashboard_id +LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard_version.created_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 1 + ORDER BY + dashboard_version.created DESC, + dashboard_version.version DESC, + dashboard.uid ASC + LIMIT 50 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_with_max_rows_last_id.sql b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_with_max_rows_last_id.sql new file mode 100755 index 00000000000..f5934ab44d1 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_with_max_rows_last_id.sql @@ -0,0 +1,35 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard_version.created as updated, + updated_user.uid as updated_by, + dashboard_version.created_by as updated_by_id, + dashboard_version.version, + dashboard_version.message, + dashboard_version.data, + dashboard_version.api_version +FROM `grafana`.`dashboard` as dashboard +LEFT OUTER JOIN `grafana`.`dashboard_version` as dashboard_version ON dashboard.id = dashboard_version.dashboard_id +LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard_version.created_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard_version.version < 500 + ORDER BY + dashboard_version.created DESC, + dashboard_version.version DESC, + dashboard.uid ASC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_with_max_rows.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_with_max_rows.sql new file mode 100755 index 00000000000..2cdb3115aec --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_with_max_rows.sql @@ -0,0 +1,31 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard.updated, + updated_user.uid as updated_by, + dashboard.updated_by as updated_by_id, + dashboard.version, + '' as message, + dashboard.data, + dashboard.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard.deleted IS NULL + ORDER BY dashboard.id DESC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_with_max_rows_last_id.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_with_max_rows_last_id.sql new file mode 100755 index 00000000000..6994bf56bd4 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-dashboard_with_max_rows_last_id.sql @@ -0,0 +1,32 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard.updated, + updated_user.uid as updated_by, + dashboard.updated_by as updated_by_id, + dashboard.version, + '' as message, + dashboard.data, + dashboard.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard.id < 500 + AND dashboard.deleted IS NULL + ORDER BY dashboard.id DESC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_with_max_rows.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_with_max_rows.sql new file mode 100755 index 00000000000..18a557c9dfd --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_with_max_rows.sql @@ -0,0 +1,34 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard_version.created as updated, + updated_user.uid as updated_by, + dashboard_version.created_by as updated_by_id, + dashboard_version.version, + dashboard_version.message, + dashboard_version.data, + dashboard_version.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard_version.created_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 1 + ORDER BY + dashboard_version.created DESC, + dashboard_version.version DESC, + dashboard.uid ASC + LIMIT 50 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_with_max_rows_last_id.sql b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_with_max_rows_last_id.sql new file mode 100755 index 00000000000..b1930157b6e --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_with_max_rows_last_id.sql @@ -0,0 +1,35 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard_version.created as updated, + updated_user.uid as updated_by, + dashboard_version.created_by as updated_by_id, + dashboard_version.version, + dashboard_version.message, + dashboard_version.data, + dashboard_version.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard_version.created_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard_version.version < 500 + ORDER BY + dashboard_version.created DESC, + dashboard_version.version DESC, + dashboard.uid ASC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_with_max_rows.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_with_max_rows.sql new file mode 100755 index 00000000000..2cdb3115aec --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_with_max_rows.sql @@ -0,0 +1,31 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard.updated, + updated_user.uid as updated_by, + dashboard.updated_by as updated_by_id, + dashboard.version, + '' as message, + dashboard.data, + dashboard.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard.deleted IS NULL + ORDER BY dashboard.id DESC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_with_max_rows_last_id.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_with_max_rows_last_id.sql new file mode 100755 index 00000000000..6994bf56bd4 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-dashboard_with_max_rows_last_id.sql @@ -0,0 +1,32 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard.updated, + updated_user.uid as updated_by, + dashboard.updated_by as updated_by_id, + dashboard.version, + '' as message, + dashboard.data, + dashboard.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard.id < 500 + AND dashboard.deleted IS NULL + ORDER BY dashboard.id DESC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_with_max_rows.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_with_max_rows.sql new file mode 100755 index 00000000000..18a557c9dfd --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_with_max_rows.sql @@ -0,0 +1,34 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard_version.created as updated, + updated_user.uid as updated_by, + dashboard_version.created_by as updated_by_id, + dashboard_version.version, + dashboard_version.message, + dashboard_version.data, + dashboard_version.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard_version.created_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 1 + ORDER BY + dashboard_version.created DESC, + dashboard_version.version DESC, + dashboard.uid ASC + LIMIT 50 diff --git a/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_with_max_rows_last_id.sql b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_with_max_rows_last_id.sql new file mode 100755 index 00000000000..b1930157b6e --- /dev/null +++ b/pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_with_max_rows_last_id.sql @@ -0,0 +1,35 @@ +SELECT + dashboard.org_id, + dashboard.id, + dashboard.uid, + dashboard.title, + dashboard.folder_uid, + dashboard.deleted, + plugin_id, + provisioning.name as repo_name, + provisioning.external_id as repo_path, + provisioning.check_sum as repo_hash, + provisioning.updated as repo_ts, + dashboard.created, + created_user.uid as created_by, + dashboard.created_by as created_by_id, + dashboard_version.created as updated, + updated_user.uid as updated_by, + dashboard_version.created_by as updated_by_id, + dashboard_version.version, + dashboard_version.message, + dashboard_version.data, + dashboard_version.api_version +FROM "grafana"."dashboard" as dashboard +LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id +LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id +LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id +LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard_version.created_by = updated_user.id +WHERE dashboard.is_folder = FALSE + AND dashboard.org_id = 2 + AND dashboard_version.version < 500 + ORDER BY + dashboard_version.created DESC, + dashboard_version.version DESC, + dashboard.uid ASC + LIMIT 100 diff --git a/pkg/registry/apis/dashboard/legacy/types.go b/pkg/registry/apis/dashboard/legacy/types.go index 26cd043e8e5..968b2eb123c 100644 --- a/pkg/registry/apis/dashboard/legacy/types.go +++ b/pkg/registry/apis/dashboard/legacy/types.go @@ -16,6 +16,11 @@ type DashboardQuery struct { UID string // to select a single dashboard Limit int + // MaxRows is used internally by the iterator to fetch data in batches + // When set, the SQL query will include LIMIT MaxRows + // If Limit is smaller, that will be used instead + MaxRows int + // Included in the continue token // This is the ID from the last dashboard sent in the previous page LastID int64 diff --git a/pkg/registry/apis/dashboard/mutation_test.go b/pkg/registry/apis/dashboard/mutation_test.go index b95ae2519b5..78c49b28e8a 100644 --- a/pkg/registry/apis/dashboard/mutation_test.go +++ b/pkg/registry/apis/dashboard/mutation_test.go @@ -22,7 +22,7 @@ import ( ) func TestDashboardAPIBuilder_Mutate(t *testing.T) { - migration.Initialize(testutil.NewDataSourceProvider(testutil.StandardTestConfig), testutil.NewLibraryElementProvider()) + migration.Initialize(testutil.NewDataSourceProvider(testutil.StandardTestConfig), testutil.NewLibraryElementProvider(), migration.DefaultCacheTTL) tests := []struct { name string inputObj runtime.Object diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index e9925f0a3b7..c0cd241c1c4 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "maps" + "strconv" "strings" "github.com/prometheus/client_golang/prometheus" @@ -195,13 +196,30 @@ func RegisterAPIService( datasourceService: datasourceService, }, &libraryElementIndexProvider{ libraryElementService: libraryPanels, - }) + }, cfg.DashboardSchemaMigrationCacheTTL) + + // For single-tenant deployments (indicated by StackID), preload the cache in the background + if cfg.StackID != "" { + // Single namespace for cloud stack + stackID, err := strconv.ParseInt(cfg.StackID, 10, 64) + if err == nil { + var nsInfo authlib.NamespaceInfo + nsInfo, err = authlib.ParseNamespace(authlib.CloudNamespaceFormatter(stackID)) + if err == nil { + migration.PreloadCacheInBackground([]authlib.NamespaceInfo{nsInfo}) + } + } + if err != nil { + logging.DefaultLogger.Error("failed to parse namespace for cache preloading", "stackId", cfg.StackID, "err", err) + } + } + apiregistration.RegisterAPI(builder) return builder } func NewAPIService(ac authlib.AccessClient, features featuremgmt.FeatureToggles, folderClientProvider client.K8sHandlerProvider, datasourceProvider schemaversion.DataSourceIndexProvider, libraryElementProvider schemaversion.LibraryElementIndexProvider, resourcePermissionsSvc *dynamic.NamespaceableResourceInterface) *DashboardsAPIBuilder { - migration.Initialize(datasourceProvider, libraryElementProvider) + migration.Initialize(datasourceProvider, libraryElementProvider, migration.DefaultCacheTTL) return &DashboardsAPIBuilder{ minRefreshInterval: "10s", accessClient: ac, @@ -757,11 +775,6 @@ func (b *DashboardsAPIBuilder) afterDelete(obj runtime.Object, _ *metav1.DeleteO } var defaultDashboardPermissions = []map[string]any{ - { - "kind": "BasicRole", - "name": "Admin", - "verb": "admin", - }, { "kind": "BasicRole", "name": "Editor", diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index ab7f05e7d00..406494b9d36 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -1103,3 +1103,7 @@ func (m *MockClient) BulkProcess(ctx context.Context, opts ...grpc.CallOption) ( func (m *MockClient) UpdateIndex(ctx context.Context, reason string) error { return nil } + +func (m *MockClient) GetQuotaUsage(ctx context.Context, req *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) { + return nil, nil +} diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index 76122fd90b5..c43ea56d4a9 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -220,11 +220,6 @@ func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.API } var defaultPermissions = []map[string]any{ - { - "kind": "BasicRole", - "name": "Admin", - "verb": "admin", - }, { "kind": "BasicRole", "name": "Editor", diff --git a/pkg/registry/apis/folders/validate.go b/pkg/registry/apis/folders/validate.go index 4f8ccd2250d..eb0c29b30a1 100644 --- a/pkg/registry/apis/folders/validate.go +++ b/pkg/registry/apis/folders/validate.go @@ -53,6 +53,7 @@ func validateOnCreate(ctx context.Context, f *folders.Folder, getter parentsGett return folder.ErrFolderCannotBeParentOfItself } + // note: `parents` will include itself as the last item parents, err := getter(ctx, f) if err != nil { return fmt.Errorf("unable to create folder inside parent: %w", err) diff --git a/pkg/registry/apis/folders/validate_test.go b/pkg/registry/apis/folders/validate_test.go index c4bf07bc71c..7fdb3cfae12 100644 --- a/pkg/registry/apis/folders/validate_test.go +++ b/pkg/registry/apis/folders/validate_test.go @@ -20,8 +20,7 @@ func TestValidateCreate(t *testing.T) { tests := []struct { name string folder *folders.Folder - getter *folders.FolderInfoList - getterError error + mockFolders map[string]*folders.Folder expectedErr string maxDepth int // defaults to 5 unless set }{ @@ -36,10 +35,23 @@ func TestValidateCreate(t *testing.T) { Title: "some title", }, }, - getter: &folders.FolderInfoList{ - Items: []folders.FolderInfo{ - {Name: "p2", Parent: "p3"}, - {Name: "p3"}, + mockFolders: map[string]*folders.Folder{ + "p2": { + ObjectMeta: metav1.ObjectMeta{ + Name: "p2", + Annotations: map[string]string{"grafana.app/folder": "p3"}, + }, + Spec: folders.FolderSpec{ + Title: "p2 title", + }, + }, + "p3": { + ObjectMeta: metav1.ObjectMeta{ + Name: "p3", + }, + Spec: folders.FolderSpec{ + Title: "p3 title", + }, }, }, }, @@ -94,12 +106,41 @@ func TestValidateCreate(t *testing.T) { Title: "some title", }, }, - getter: &folders.FolderInfoList{ - Items: []folders.FolderInfo{ - {Name: "p2", Parent: "p3"}, - {Name: "p3", Parent: "p4"}, - {Name: "p4", Parent: folder.GeneralFolderUID}, - {Name: folder.GeneralFolderUID}, + mockFolders: map[string]*folders.Folder{ + "p2": { + ObjectMeta: metav1.ObjectMeta{ + Name: "p2", + Annotations: map[string]string{"grafana.app/folder": "p3"}, + }, + Spec: folders.FolderSpec{ + Title: "p2 title", + }, + }, + "p3": { + ObjectMeta: metav1.ObjectMeta{ + Name: "p3", + Annotations: map[string]string{"grafana.app/folder": "p4"}, + }, + Spec: folders.FolderSpec{ + Title: "p3 title", + }, + }, + "p4": { + ObjectMeta: metav1.ObjectMeta{ + Name: "p4", + Annotations: map[string]string{"grafana.app/folder": folder.GeneralFolderUID}, + }, + Spec: folders.FolderSpec{ + Title: "p4 title", + }, + }, + folder.GeneralFolderUID: { + ObjectMeta: metav1.ObjectMeta{ + Name: folder.GeneralFolderUID, + }, + Spec: folders.FolderSpec{ + Title: "General", + }, }, }, maxDepth: 2, @@ -116,17 +157,95 @@ func TestValidateCreate(t *testing.T) { Title: "some title", }, }, - getter: &folders.FolderInfoList{ - Items: []folders.FolderInfo{ - {Name: "4", Parent: "3"}, - {Name: "3", Parent: "2"}, - {Name: "2", Parent: "1"}, - {Name: "1", Parent: folder.GeneralFolderUID}, - {Name: folder.GeneralFolderUID}, + mockFolders: map[string]*folders.Folder{ + "4": { + ObjectMeta: metav1.ObjectMeta{ + Name: "4", + Annotations: map[string]string{"grafana.app/folder": "3"}, + }, + Spec: folders.FolderSpec{ + Title: "4 title", + }, + }, + "3": { + ObjectMeta: metav1.ObjectMeta{ + Name: "3", + Annotations: map[string]string{"grafana.app/folder": "2"}, + }, + Spec: folders.FolderSpec{ + Title: "3 title", + }, + }, + "2": { + ObjectMeta: metav1.ObjectMeta{ + Name: "2", + Annotations: map[string]string{"grafana.app/folder": "1"}, + }, + Spec: folders.FolderSpec{ + Title: "2 title", + }, + }, + "1": { + ObjectMeta: metav1.ObjectMeta{ + Name: "1", + }, + Spec: folders.FolderSpec{ + Title: "1 title", + }, }, }, maxDepth: folder.MaxNestedFolderDepth, }, + { + name: "cannot create a circular reference", + folder: &folders.Folder{ + ObjectMeta: metav1.ObjectMeta{ + Name: "3", + Annotations: map[string]string{"grafana.app/folder": "2"}, + }, + Spec: folders.FolderSpec{ + Title: "some title", + }, + }, + expectedErr: "cyclic folder references found", + mockFolders: map[string]*folders.Folder{ + "2": { + ObjectMeta: metav1.ObjectMeta{ + Name: "2", + Annotations: map[string]string{"grafana.app/folder": "1"}, + }, + Spec: folders.FolderSpec{ + Title: "2 title", + }, + }, + "1": { + ObjectMeta: metav1.ObjectMeta{ + Name: "1", + Annotations: map[string]string{"grafana.app/folder": "3"}, + }, + Spec: folders.FolderSpec{ + Title: "1 title", + }, + }, + "3": { + ObjectMeta: metav1.ObjectMeta{ + Name: "3", + Annotations: map[string]string{"grafana.app/folder": folder.GeneralFolderUID}, + }, + Spec: folders.FolderSpec{ + Title: "3 title", + }, + }, + folder.GeneralFolderUID: { + ObjectMeta: metav1.ObjectMeta{ + Name: folder.GeneralFolderUID, + }, + Spec: folders.FolderSpec{ + Title: "General", + }, + }, + }, + }, } for _, tt := range tests { @@ -135,10 +254,16 @@ func TestValidateCreate(t *testing.T) { if maxDepth == 0 { maxDepth = 5 } - err := validateOnCreate(context.Background(), tt.folder, - func(ctx context.Context, folder *folders.Folder) (*folders.FolderInfoList, error) { - return tt.getter, tt.getterError - }, maxDepth) + + mockStorage := grafanarest.NewMockStorage(t) + for name, f := range tt.mockFolders { + f.Name = name + mockStorage.On("Get", context.Background(), name, &metav1.GetOptions{}).Return(f, nil).Maybe() + } + + getter := newParentsGetter(mockStorage, maxDepth) + + err := validateOnCreate(context.Background(), tt.folder, getter, maxDepth) if tt.expectedErr == "" { require.NoError(t, err) diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index b9da9e6f2c5..85d8bc4e4f1 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -90,11 +90,11 @@ func RegisterAPIService( builder := &IdentityAccessManagementAPIBuilder{ store: store, - userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation), - saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation), - legacyTeamStore: team.NewLegacyStore(store, legacyAccessClient, enableAuthnMutation), - teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation), - ssoLegacyStore: sso.NewLegacyStore(ssoService), + userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing), + saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing), + legacyTeamStore: team.NewLegacyStore(store, legacyAccessClient, enableAuthnMutation, tracing), + teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation, tracing), + ssoLegacyStore: sso.NewLegacyStore(ssoService, tracing), coreRolesStorage: coreRolesStorage, rolesStorage: rolesStorage, resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider), @@ -114,7 +114,7 @@ func RegisterAPIService( dual: dual, unified: unified, userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(), - unified, user.NewUserLegacySearchClient(userService), features), + unified, user.NewUserLegacySearchClient(userService, tracing), features), teamSearch: NewTeamSearchHandler(tracing, dual, team.NewLegacyTeamSearchClient(teamService), unified, features), } apiregistration.RegisterAPI(builder) diff --git a/pkg/registry/apis/iam/serviceaccount/store.go b/pkg/registry/apis/iam/serviceaccount/store.go index 2980322cb27..db4709a4e2e 100644 --- a/pkg/registry/apis/iam/serviceaccount/store.go +++ b/pkg/registry/apis/iam/serviceaccount/store.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -35,14 +36,15 @@ var ( var resource = iamv0alpha1.ServiceAccountResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore { - return &LegacyStore{store, ac, enableAuthnMutation} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{store, ac, enableAuthnMutation, tracer} } type LegacyStore struct { store legacy.LegacyIdentityStore ac claims.AccessClient enableAuthnMutation bool + tracer trace.Tracer } // DeleteCollection implements rest.CollectionDeleter. @@ -52,6 +54,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res // Delete implements rest.GracefulDeleter. func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.Delete") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete") } @@ -95,6 +100,9 @@ func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.Upda // Create implements rest.Creater. func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.Create") + defer span.End() + if !s.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create") } @@ -165,6 +173,9 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.List") + defer span.End() + res, err := common.List( ctx, resource, s.ac, common.PaginationFromListOptions(options), func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.ServiceAccount], error) { @@ -228,6 +239,9 @@ func extractPluginNameFromTitle(title string) string { } func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "serviceaccount.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err diff --git a/pkg/registry/apis/iam/sso/store.go b/pkg/registry/apis/iam/sso/store.go index c9965adc152..9ab09fa6ef4 100644 --- a/pkg/registry/apis/iam/sso/store.go +++ b/pkg/registry/apis/iam/sso/store.go @@ -7,6 +7,7 @@ import ( commonv1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,12 +33,13 @@ var ( var resource = iamv0.SSOSettingResourceInfo -func NewLegacyStore(service ssosettings.Service) *LegacyStore { - return &LegacyStore{service} +func NewLegacyStore(service ssosettings.Service, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{service, tracer} } type LegacyStore struct { service ssosettings.Service + tracer trace.Tracer } // Destroy implements rest.Storage. @@ -71,6 +73,9 @@ func (s *LegacyStore) NewList() runtime.Object { // List implements rest.Lister. func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "sso.List") + defer span.End() + ns, _ := request.NamespaceInfoFrom(ctx, false) settings, err := s.service.List(ctx) @@ -88,6 +93,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt // Get implements rest.Getter. func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "sso.Get") + defer span.End() + ns, _ := request.NamespaceInfoFrom(ctx, false) setting, err := s.service.GetForProviderWithRedactedSecrets(ctx, name) @@ -112,6 +120,9 @@ func (s *LegacyStore) Update( _ bool, _ *metav1.UpdateOptions, ) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "sso.Update") + defer span.End() + const created = false ident, err := identity.GetRequester(ctx) if err != nil { @@ -148,6 +159,9 @@ func (s *LegacyStore) Delete( _ rest.ValidateObjectFunc, options *metav1.DeleteOptions, ) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "sso.Delete") + defer span.End() + obj, err := s.Get(ctx, name, nil) if err != nil { return obj, false, err diff --git a/pkg/registry/apis/iam/team/store.go b/pkg/registry/apis/iam/team/store.go index e048fdb7a6d..c667c8ec374 100644 --- a/pkg/registry/apis/iam/team/store.go +++ b/pkg/registry/apis/iam/team/store.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" + "go.opentelemetry.io/otel/trace" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -35,14 +36,15 @@ var ( var resource = iamv0alpha1.TeamResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore { - return &LegacyStore{store, ac, enableAuthnMutation} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{store, ac, enableAuthnMutation, tracer} } type LegacyStore struct { store legacy.LegacyIdentityStore ac claims.AccessClient enableAuthnMutation bool + tracer trace.Tracer } func (s *LegacyStore) New() runtime.Object { @@ -74,6 +76,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res // Delete implements rest.GracefulDeleter. func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "team.Delete") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete") } @@ -112,6 +117,9 @@ 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) { + ctx, span := s.tracer.Start(ctx, "team.Update") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "update") } @@ -161,6 +169,9 @@ func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.Upda } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "team.List") + defer span.End() + res, err := common.List( ctx, resource, s.ac, common.PaginationFromListOptions(options), func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.Team], error) { @@ -197,6 +208,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt } func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "team.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -219,6 +233,9 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO } func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "team.Create") + defer span.End() + if !s.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create") } diff --git a/pkg/registry/apis/iam/team_search_test.go b/pkg/registry/apis/iam/team_search_test.go index ccc1abbf18c..76efed1c067 100644 --- a/pkg/registry/apis/iam/team_search_test.go +++ b/pkg/registry/apis/iam/team_search_test.go @@ -284,3 +284,6 @@ func (m *MockClient) BulkProcess(ctx context.Context, opts ...grpc.CallOption) ( func (m *MockClient) UpdateIndex(ctx context.Context, reason string) error { return nil } +func (m *MockClient) GetQuotaUsage(ctx context.Context, in *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) { + return nil, nil +} diff --git a/pkg/registry/apis/iam/teambinding/store.go b/pkg/registry/apis/iam/teambinding/store.go index eaaa4766e39..6f6fd4ce162 100644 --- a/pkg/registry/apis/iam/teambinding/store.go +++ b/pkg/registry/apis/iam/teambinding/store.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "go.opentelemetry.io/otel/trace" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -35,13 +36,14 @@ var ( _ rest.CollectionDeleter = (*LegacyBindingStore)(nil) ) -func NewLegacyBindingStore(store legacy.LegacyIdentityStore, enableAuthnMutation bool) *LegacyBindingStore { - return &LegacyBindingStore{store, enableAuthnMutation} +func NewLegacyBindingStore(store legacy.LegacyIdentityStore, enableAuthnMutation bool, tracer trace.Tracer) *LegacyBindingStore { + return &LegacyBindingStore{store, enableAuthnMutation, tracer} } type LegacyBindingStore struct { store legacy.LegacyIdentityStore enableAuthnMutation bool + tracer trace.Tracer } // Destroy implements rest.Storage. @@ -73,6 +75,9 @@ func (l *LegacyBindingStore) ConvertToTable(ctx context.Context, object runtime. } func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Update") + defer span.End() + if !l.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "update") } @@ -125,6 +130,9 @@ func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo re } func (l *LegacyBindingStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Delete") + defer span.End() + if !l.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "delete") } @@ -160,6 +168,9 @@ func (l *LegacyBindingStore) DeleteCollection(ctx context.Context, deleteValidat } func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Create") + defer span.End() + if !l.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "create") } @@ -230,6 +241,9 @@ func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, cre // Get implements rest.Getter. func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -254,6 +268,9 @@ func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *meta // List implements rest.Lister. func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := l.tracer.Start(ctx, "teambinding.List") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err diff --git a/pkg/registry/apis/iam/user/legacy_search.go b/pkg/registry/apis/iam/user/legacy_search.go index 534a4f016eb..7fb6c13f7a6 100644 --- a/pkg/registry/apis/iam/user/legacy_search.go +++ b/pkg/registry/apis/iam/user/legacy_search.go @@ -6,6 +6,7 @@ import ( "log/slog" "math" + "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -27,13 +28,15 @@ type UserLegacySearchClient struct { resourcepb.ResourceIndexClient userService user.Service log *slog.Logger + tracer trace.Tracer } // NewUserLegacySearchClient creates a new UserLegacySearchClient. -func NewUserLegacySearchClient(userService user.Service) *UserLegacySearchClient { +func NewUserLegacySearchClient(userService user.Service, tracer trace.Tracer) *UserLegacySearchClient { return &UserLegacySearchClient{ userService: userService, log: slog.Default().With("logger", "legacy-user-search-client"), + tracer: tracer, } } @@ -41,6 +44,9 @@ func NewUserLegacySearchClient(userService user.Service) *UserLegacySearchClient // It only supports exact matching for title, login, or email. // FIXME: This implementation only supports a single field query and will be extended in the future. func (c *UserLegacySearchClient) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest, _ ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) { + ctx, span := c.tracer.Start(ctx, "user.Search") + defer span.End() + signedInUser, err := identity.GetRequester(ctx) if err != nil { return nil, err diff --git a/pkg/registry/apis/iam/user/legacy_search_test.go b/pkg/registry/apis/iam/user/legacy_search_test.go index b8a754f34ce..6bd49185786 100644 --- a/pkg/registry/apis/iam/user/legacy_search_test.go +++ b/pkg/registry/apis/iam/user/legacy_search_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" res "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -17,7 +18,7 @@ import ( func TestUserLegacySearchClient_Search(t *testing.T) { t.Run("should return error if no query fields are provided", func(t *testing.T) { mockUserService := usertest.NewMockService(t) - client := NewUserLegacySearchClient(mockUserService) + client := NewUserLegacySearchClient(mockUserService, tracing.NewNoopTracerService()) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1}) req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ @@ -66,7 +67,7 @@ func TestUserLegacySearchClient_Search(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { mockUserService := usertest.NewMockService(t) - client := NewUserLegacySearchClient(mockUserService) + client := NewUserLegacySearchClient(mockUserService, tracing.NewNoopTracerService()) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1}) req := &resourcepb.ResourceSearchRequest{ Limit: 10, @@ -125,7 +126,7 @@ func TestUserLegacySearchClient_Search(t *testing.T) { t.Run("title should have precedence over login and email", func(t *testing.T) { mockUserService := usertest.NewMockService(t) - client := NewUserLegacySearchClient(mockUserService) + client := NewUserLegacySearchClient(mockUserService, tracing.NewNoopTracerService()) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1}) req := &resourcepb.ResourceSearchRequest{ Options: &resourcepb.ListOptions{ diff --git a/pkg/registry/apis/iam/user/store.go b/pkg/registry/apis/iam/user/store.go index 803b2972b85..97daee3af18 100644 --- a/pkg/registry/apis/iam/user/store.go +++ b/pkg/registry/apis/iam/user/store.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "go.opentelemetry.io/otel/trace" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -36,18 +37,22 @@ var ( var resource = iamv0alpha1.UserResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore { - return &LegacyStore{store, ac, enableAuthnMutation} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore { + return &LegacyStore{store, ac, enableAuthnMutation, tracer} } type LegacyStore struct { store legacy.LegacyIdentityStore ac claims.AccessClient enableAuthnMutation bool + tracer trace.Tracer } // 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) { + ctx, span := s.tracer.Start(ctx, "user.Update") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "update") } @@ -105,6 +110,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res // Delete implements rest.GracefulDeleter. func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + ctx, span := s.tracer.Start(ctx, "user.Delete") + defer span.End() + if !s.enableAuthnMutation { return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete") } @@ -171,6 +179,9 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "user.List") + defer span.End() + res, err := common.List( ctx, resource, s.ac, common.PaginationFromListOptions(options), func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.User], error) { @@ -206,6 +217,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt } func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "user.Get") + defer span.End() + ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err @@ -229,6 +243,9 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO // Create implements rest.Creater. func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + ctx, span := s.tracer.Start(ctx, "user.Create") + defer span.End() + if !s.enableAuthnMutation { return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create") } diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index 99323eb2721..ad9c9ac9888 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -10,6 +10,8 @@ import ( "strconv" "strings" + "github.com/grafana/authlib/authn" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" "github.com/grafana/grafana/pkg/api/dtos" @@ -154,11 +156,11 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O } } } - connectLogger.Debug("responder sending status code", "statusCode", statusCode) + connectLogger.Debug("responder sending status code", "statusCode", statusCode, "caller", getCaller(ctx)) }, func(err error) { - connectLogger.Error("error caught in handler", "err", err) + connectLogger.Error("error caught in handler", "err", err, "caller", getCaller(ctx)) span.SetStatus(codes.Error, "query error") if err == nil { @@ -480,3 +482,12 @@ func getValidDataSourceRef(ctx context.Context, ds *v0alpha1.DataSourceRef, id i return ds, nil } + +func getCaller(ctx context.Context) string { + authInfo, ok := claims.AuthInfoFrom(ctx) + if !ok { + return "" + } else { + return strings.Join(authInfo.GetExtra()[authn.ServiceIdentityKey], ",") + } +} diff --git a/pkg/registry/apis/query/query_test.go b/pkg/registry/apis/query/query_test.go index b23c78d55bc..56025d3893b 100644 --- a/pkg/registry/apis/query/query_test.go +++ b/pkg/registry/apis/query/query_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" dataapi "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" @@ -180,7 +181,7 @@ func TestQueryAPI(t *testing.T) { legacyDatasourceLookup: &mockLegacyDataSourceLookup{}, } - reqCtx := identity.WithRequester(context.Background(), mockUser{}) + reqCtx := claims.WithAuthInfo(identity.WithRequester(context.Background(), mockUser{}), &mockAuthInfo{}) req := httptest.NewRequestWithContext(reqCtx, http.MethodPost, "/some-path", bytes.NewReader([]byte(tc.queryJSON))) req.Header.Set("Content-Type", "application/json") @@ -439,3 +440,11 @@ func TestMergeHeaders(t *testing.T) { }) } } + +type mockAuthInfo struct { + claims.AuthInfo +} + +func (main mockAuthInfo) GetExtra() map[string][]string { + return nil +} diff --git a/pkg/registry/apis/service/register.go b/pkg/registry/apis/service/register.go index 7f51ad844e0..1002bff5bf6 100644 --- a/pkg/registry/apis/service/register.go +++ b/pkg/registry/apis/service/register.go @@ -12,6 +12,7 @@ import ( service "github.com/grafana/grafana/pkg/apis/service/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + roleauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" ) @@ -37,7 +38,7 @@ func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration bui } func (b *ServiceAPIBuilder) GetAuthorizer() authorizer.Authorizer { - return nil // default authorizer is fine + return roleauthorizer.NewRoleAuthorizer() } func (b *ServiceAPIBuilder) GetGroupVersion() schema.GroupVersion { diff --git a/pkg/registry/apps/apps.go b/pkg/registry/apps/apps.go index 9ea31109495..a1ec8aafd65 100644 --- a/pkg/registry/apps/apps.go +++ b/pkg/registry/apps/apps.go @@ -3,6 +3,8 @@ package appregistry import ( "context" + "github.com/grafana/grafana/pkg/registry/apps/quotas" + "github.com/open-feature/go-sdk/openfeature" "k8s.io/client-go/rest" "github.com/grafana/grafana-app-sdk/app" @@ -44,12 +46,17 @@ func ProvideAppInstallers( exampleAppInstaller *example.ExampleAppInstaller, advisorAppInstaller *advisor.AdvisorAppInstaller, alertingHistorianAppInstaller *historian.AlertingHistorianAppInstaller, + quotasAppInstaller *quotas.QuotasAppInstaller, ) []appsdkapiserver.AppInstaller { + featureClient := openfeature.NewDefaultClient() installers := []appsdkapiserver.AppInstaller{ playlistAppInstaller, pluginsApplInstaller, exampleAppInstaller, } + if featureClient.Boolean(context.Background(), featuremgmt.FlagKubernetesUnifiedStorageQuotas, false, openfeature.TransactionContext(context.Background())) { + installers = append(installers, quotasAppInstaller) + } //nolint:staticcheck // not yet migrated to OpenFeature if features.IsEnabledGlobally(featuremgmt.FlagKubernetesShortURLs) { installers = append(installers, shorturlAppInstaller) diff --git a/pkg/registry/apps/apps_test.go b/pkg/registry/apps/apps_test.go index 6a6f0c9a2aa..737e7c8aa9a 100644 --- a/pkg/registry/apps/apps_test.go +++ b/pkg/registry/apps/apps_test.go @@ -3,6 +3,7 @@ package appregistry import ( "testing" + "github.com/grafana/grafana/pkg/registry/apps/quotas" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/registry/apps/advisor" @@ -27,6 +28,7 @@ func TestProvideAppInstallers_Table(t *testing.T) { exampleAppInstaller := &example.ExampleAppInstaller{} advisorAppInstaller := &advisor.AdvisorAppInstaller{} historianAppInstaller := &historian.AlertingHistorianAppInstaller{} + quotasAppInstaller := "as.QuotasAppInstaller{} tests := []struct { name string @@ -43,7 +45,7 @@ func TestProvideAppInstallers_Table(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { features := featuremgmt.WithFeatures(tt.flags...) - got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller, notificationsAppInstaller, nil, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, historianAppInstaller) + got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller, notificationsAppInstaller, nil, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, historianAppInstaller, quotasAppInstaller) if tt.expectRulesApp { require.Contains(t, got, tt.rulesInst) } else { diff --git a/pkg/registry/apps/plugins/accesscontrol.go b/pkg/registry/apps/plugins/accesscontrol.go index d41efa86f97..e0a8c9b6c23 100644 --- a/pkg/registry/apps/plugins/accesscontrol.go +++ b/pkg/registry/apps/plugins/accesscontrol.go @@ -13,15 +13,15 @@ const ( ActionPluginsPluginsDelete = "plugins.plugins:delete" // DELETE. // PluginMetas - ActionPluginsPluginsMetaCreate = "plugins.pluginsmeta:create" // CREATE. - ActionPluginsPluginsMetaWrite = "plugins.pluginsmeta:write" // UPDATE. - ActionPluginsPluginsMetaRead = "plugins.pluginsmeta:read" // GET + LIST. - ActionPluginsPluginsMetaDelete = "plugins.pluginsmeta:delete" // DELETE. + ActionPluginsPluginsMetaCreate = "plugins.metas:create" // CREATE. + ActionPluginsPluginsMetaWrite = "plugins.metas:write" // UPDATE. + ActionPluginsPluginsMetaRead = "plugins.metas:read" // GET + LIST. + ActionPluginsPluginsMetaDelete = "plugins.metas:delete" // DELETE. ) var ( ScopeProviderPluginsPlugins = accesscontrol.NewScopeProvider("plugins.plugins") - ScopeProviderPluginsPluginsMeta = accesscontrol.NewScopeProvider("plugins.pluginsmeta") + ScopeProviderPluginsPluginsMeta = accesscontrol.NewScopeProvider("plugins.metas") ScopeAllPluginsPlugins = ScopeProviderPluginsPlugins.GetResourceAllScope() ScopeAllPluginsPluginsMeta = ScopeProviderPluginsPluginsMeta.GetResourceAllScope() @@ -76,7 +76,7 @@ func registerAccessControlRoles(service accesscontrol.Service) error { // PluginMetas pluginsMetaReader := accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ - Name: "fixed:plugins.pluginsmeta:reader", + Name: "fixed:plugins.metas:reader", DisplayName: "Plugin Metas Reader", Description: "Read and list plugin metadata.", Group: "Plugins", @@ -92,7 +92,7 @@ func registerAccessControlRoles(service accesscontrol.Service) error { pluginsMetaWriter := accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ - Name: "fixed:plugins.pluginsmeta:writer", + Name: "fixed:plugins.metas:writer", DisplayName: "Plugin Metas Writer", Description: "Create, update and delete plugin metadata.", Group: "Plugins", diff --git a/pkg/registry/apps/plugins/register.go b/pkg/registry/apps/plugins/register.go index 6831d31ef9b..9113d927a29 100644 --- a/pkg/registry/apps/plugins/register.go +++ b/pkg/registry/apps/plugins/register.go @@ -6,12 +6,12 @@ import ( authlib "github.com/grafana/authlib/types" appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" - "k8s.io/apiserver/pkg/authorization/authorizer" pluginsapp "github.com/grafana/grafana/apps/plugins/pkg/app" "github.com/grafana/grafana/apps/plugins/pkg/app/meta" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/appinstaller" + grafanaauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" ) var ( @@ -34,21 +34,16 @@ func ProvideAppInstaller(accessControlService accesscontrol.Service, accessClien } coreProvider := meta.NewCoreProvider() - cloudProvider := meta.NewCloudProvider(grafanaComAPIURL) + cloudProvider := meta.NewCatalogProvider(grafanaComAPIURL) metaProviderManager := meta.NewProviderManager(coreProvider, cloudProvider) - i, err := pluginsapp.ProvideAppInstaller(metaProviderManager) + authorizer := grafanaauthorizer.NewResourceAuthorizer(accessClient) + i, err := pluginsapp.ProvideAppInstaller(authorizer, metaProviderManager) if err != nil { return nil, err } - i.WithAccessChecker(accessClient) - return &AppInstaller{ PluginAppInstaller: i, }, nil } - -func (a *AppInstaller) GetAuthorizer() authorizer.Authorizer { - return pluginsapp.GetAuthorizer() -} diff --git a/pkg/registry/apps/quotas/register.go b/pkg/registry/apps/quotas/register.go new file mode 100644 index 00000000000..b81d4fef5cf --- /dev/null +++ b/pkg/registry/apps/quotas/register.go @@ -0,0 +1,50 @@ +package quotas + +import ( + "github.com/grafana/grafana/apps/quotas/pkg/apis" + "github.com/grafana/grafana/pkg/storage/unified/resource" + restclient "k8s.io/client-go/rest" + + "github.com/grafana/grafana-app-sdk/app" + appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" + "github.com/grafana/grafana-app-sdk/simple" + quotasapp "github.com/grafana/grafana/apps/quotas/pkg/app" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" +) + +var ( + _ appsdkapiserver.AppInstaller = (*QuotasAppInstaller)(nil) +) + +type QuotasAppInstaller struct { + appsdkapiserver.AppInstaller + cfg *setting.Cfg +} + +func RegisterAppInstaller( + cfg *setting.Cfg, + features featuremgmt.FeatureToggles, + resourceClient resource.ResourceClient, +) (*QuotasAppInstaller, error) { + installer := &QuotasAppInstaller{ + cfg: cfg, + } + specificConfig := "asapp.QuotasAppConfig{ + ResourceClient: resourceClient, + } + provider := simple.NewAppProvider(apis.LocalManifest(), specificConfig, quotasapp.New) + + appConfig := app.Config{ + KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method + ManifestData: *apis.LocalManifest().ManifestData, + SpecificConfig: specificConfig, + } + i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, apis.NewGoTypeAssociator()) + if err != nil { + return nil, err + } + installer.AppInstaller = i + + return installer, nil +} diff --git a/pkg/registry/apps/wireset.go b/pkg/registry/apps/wireset.go index c57fe84d018..a91fa138309 100644 --- a/pkg/registry/apps/wireset.go +++ b/pkg/registry/apps/wireset.go @@ -2,6 +2,7 @@ package appregistry import ( "github.com/google/wire" + "github.com/grafana/grafana/pkg/registry/apps/quotas" "github.com/grafana/grafana/pkg/registry/apps/alerting/historian" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" @@ -29,5 +30,6 @@ var WireSet = wire.NewSet( historian.RegisterAppInstaller, logsdrilldown.RegisterAppInstaller, annotation.RegisterAppInstaller, + quotas.RegisterAppInstaller, example.RegisterAppInstaller, ) diff --git a/pkg/registry/backgroundsvcs/adapter/service_test.go b/pkg/registry/backgroundsvcs/adapter/service_test.go index ed357d66443..275068967c0 100644 --- a/pkg/registry/backgroundsvcs/adapter/service_test.go +++ b/pkg/registry/backgroundsvcs/adapter/service_test.go @@ -55,15 +55,9 @@ func TestServiceAdapter_ErrorHandling(t *testing.T) { adapter := asNamedService(mockSvc) - t.Cleanup(func() { - adapter.StopAsync() - err := adapter.AwaitTerminated(context.Background()) - require.ErrorIs(t, err, expectedErr) - }) - err := adapter.StartAsync(context.Background()) require.NoError(t, err) - err = adapter.AwaitRunning(context.Background()) + err = adapter.AwaitTerminated(context.Background()) require.ErrorIs(t, err, expectedErr) require.True(t, mockSvc.runCalled) }) @@ -95,14 +89,9 @@ func TestServiceAdapter_ErrorHandling(t *testing.T) { adapter := asNamedService(mockSvc) - t.Cleanup(func() { - adapter.StopAsync() - err := adapter.AwaitTerminated(context.Background()) - require.ErrorIs(t, err, expectedErr) - }) err := adapter.StartAsync(context.Background()) require.NoError(t, err) - err = adapter.AwaitRunning(context.Background()) + err = adapter.AwaitTerminated(context.Background()) require.ErrorIs(t, err, expectedErr) require.True(t, mockSvc.runCalled) }) diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index d3cf6da6b5a..8500130aeea 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -89,6 +89,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apps/logsdrilldown" "github.com/grafana/grafana/pkg/registry/apps/playlist" "github.com/grafana/grafana/pkg/registry/apps/plugins" + "github.com/grafana/grafana/pkg/registry/apps/quotas" "github.com/grafana/grafana/pkg/registry/apps/shorturl" "github.com/grafana/grafana/pkg/registry/backgroundsvcs" "github.com/grafana/grafana/pkg/registry/usagestatssvcs" @@ -824,7 +825,11 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller) + quotasAppInstaller, err := quotas.RegisterAppInstaller(cfg, featureToggles, resourceClient) + if err != nil { + return nil, err + } + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller, quotasAppInstaller) builderMetrics := builder.ProvideBuilderMetrics(registerer) apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics) if err != nil { @@ -1477,7 +1482,11 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller) + quotasAppInstaller, err := quotas.RegisterAppInstaller(cfg, featureToggles, resourceClient) + if err != nil { + return nil, err + } + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller, quotasAppInstaller) builderMetrics := builder.ProvideBuilderMetrics(registerer) apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics) if err != nil { diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index dfda78ac9f0..ec67cd03c64 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -447,6 +447,7 @@ const ( ActionAlertingNotificationsTemplatesRead = "alert.notifications.templates:read" ActionAlertingNotificationsTemplatesWrite = "alert.notifications.templates:write" ActionAlertingNotificationsTemplatesDelete = "alert.notifications.templates:delete" + ActionAlertingNotificationsTemplatesTest = "alert.notifications.templates.test:write" // Alerting notifications time interval actions ActionAlertingNotificationsTimeIntervalsRead = "alert.notifications.time-intervals:read" diff --git a/pkg/services/accesscontrol/permreg/permreg.go b/pkg/services/accesscontrol/permreg/permreg.go index c9f010c2909..e975681ff03 100644 --- a/pkg/services/accesscontrol/permreg/permreg.go +++ b/pkg/services/accesscontrol/permreg/permreg.go @@ -85,7 +85,7 @@ func newPermissionRegistry() *permissionRegistry { "orgs": "orgs:id:", "plugins": "plugins:id:", "plugins.plugins": "plugins.plugins:uid:", - "plugins.pluginsmeta": "plugins.pluginsmeta:uid:", + "plugins.metas": "plugins.metas:uid:", "provisioners": "provisioners:", "reports": "reports:id:", "permissions": "permissions:type:", diff --git a/pkg/services/apiserver/appinstaller/installer.go b/pkg/services/apiserver/appinstaller/installer.go index b6e9145ff15..c7b926bd5f3 100644 --- a/pkg/services/apiserver/appinstaller/installer.go +++ b/pkg/services/apiserver/appinstaller/installer.go @@ -108,6 +108,9 @@ func RegisterAuthorizers( if authorizerProvider, ok := installer.(AuthorizerProvider); ok { authorizer := authorizerProvider.GetAuthorizer() for _, gv := range installer.GroupVersions() { + if authorizer == nil { + panic("authorizer cannot be nil for api group: " + gv.String()) + } registrar.Register(gv, authorizer) logger.Debug("Registered authorizer", "group", gv.Group, "version", gv.Version, "app") } diff --git a/pkg/services/apiserver/auth/authorizer/authorizer.go b/pkg/services/apiserver/auth/authorizer/authorizer.go index 54ea8c081d7..f58c1d14bf8 100644 --- a/pkg/services/apiserver/auth/authorizer/authorizer.go +++ b/pkg/services/apiserver/auth/authorizer/authorizer.go @@ -42,7 +42,9 @@ func NewGrafanaBuiltInSTAuthorizer(cfg *setting.Cfg) *GrafanaAuthorizer { // org role is last -- and will return allow for verbs that match expectations // The apiVersion flavors will run first and can return early when FGAC has appropriate rules - authorizers = append(authorizers, newRoleAuthorizer()) + // NOTE: role authorizer is now used by some api groups as their specific authorizer + // but there are still some apis not directly registered in the embedded delegate that benefit from including it here + authorizers = append(authorizers, NewRoleAuthorizer()) return &GrafanaAuthorizer{ apis: apis, auth: union.New(authorizers...), diff --git a/pkg/services/apiserver/auth/authorizer/resource.go b/pkg/services/apiserver/auth/authorizer/resource.go index b86f6b40f09..f2f959f9381 100644 --- a/pkg/services/apiserver/auth/authorizer/resource.go +++ b/pkg/services/apiserver/auth/authorizer/resource.go @@ -9,13 +9,13 @@ import ( claims "github.com/grafana/authlib/types" ) -func NewResourceAuthorizer(c claims.AccessClient) authorizer.Authorizer { +func NewResourceAuthorizer(c claims.AccessChecker) authorizer.Authorizer { return ResourceAuthorizer{c} } // ResourceAuthorizer is used to translate authorizer.Authorizer calls to claims.AccessClient calls type ResourceAuthorizer struct { - c claims.AccessClient + c claims.AccessChecker } func (r ResourceAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) { diff --git a/pkg/services/apiserver/auth/authorizer/role.go b/pkg/services/apiserver/auth/authorizer/role.go index 39b3b440665..23164dbf556 100644 --- a/pkg/services/apiserver/auth/authorizer/role.go +++ b/pkg/services/apiserver/auth/authorizer/role.go @@ -19,7 +19,7 @@ var orgRoleNoneAsViewerAPIGroups = []string{ type roleAuthorizer struct{} -func newRoleAuthorizer() *roleAuthorizer { +func NewRoleAuthorizer() *roleAuthorizer { return &roleAuthorizer{} } diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 605016c112d..ac38dee7e3f 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -275,6 +275,8 @@ func (s *service) start(ctx context.Context) error { auth := a.GetAuthorizer() if auth != nil { s.authorizer.Register(gv, auth) + } else { + panic("authorizer can not be nil for api group=" + gv.String()) } } } diff --git a/pkg/services/authz/rbac/mapper.go b/pkg/services/authz/rbac/mapper.go index e34965df999..d50ca050cdb 100644 --- a/pkg/services/authz/rbac/mapper.go +++ b/pkg/services/authz/rbac/mapper.go @@ -298,8 +298,8 @@ func NewMapperRegistry() MapperRegistry { }, }, "plugins.grafana.app": { - "plugins": newResourceTranslation("plugins.plugins", "uid", false, nil), - "pluginsmeta": newResourceTranslation("plugins.pluginsmeta", "uid", false, nil), + "plugins": newResourceTranslation("plugins.plugins", "uid", false, nil), + "metas": newResourceTranslation("plugins.metas", "uid", false, nil), }, }) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 680680791a5..c1b318c97b4 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -414,6 +414,13 @@ var ( Owner: grafanaDataProSquad, RequiresRestart: true, }, + { + Name: "kubernetesUnifiedStorageQuotas", + Description: "Adds support for Kubernetes unified storage quotas", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + RequiresRestart: true, + }, { Name: "kubernetesLogsDrilldown", Description: "Adds support for Kubernetes logs drilldown", @@ -1624,6 +1631,14 @@ var ( Owner: grafanaFrontendSearchNavOrganise, Expression: "false", }, + { + Name: "recentlyViewedDashboards", + Description: "Enables recently viewed dashboards section in the browsing dashboard page", + Stage: FeatureStageExperimental, + Owner: grafanaFrontendSearchNavOrganise, + FrontendOnly: true, + Expression: "false", + }, { Name: "alertEnrichment", Description: "Enable configuration of alert enrichments in Grafana Cloud.", @@ -2015,6 +2030,13 @@ var ( FrontendOnly: true, Expression: "false", }, + { + Name: "multiPropsVariables", + Description: "Enables support for variables whose values can have multiple properties", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDashboardsSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles-gitlog.csv b/pkg/services/featuremgmt/toggles-gitlog.csv index 3f34ca9edc9..c924b05d7d5 100644 --- a/pkg/services/featuremgmt/toggles-gitlog.csv +++ b/pkg/services/featuremgmt/toggles-gitlog.csv @@ -244,7 +244,6 @@ addFieldFromCalculationStatFunctions,2023-11-03T14:39:58Z,2025-11-17T15:58:43Z,6 pdfTables,2023-11-06T13:39:22Z,,95b48339f89c7d267bce7d894404d26ccd75d0e3,Agnès Toulet newVizTooltips,2023-11-06T16:35:59Z,2024-04-03T00:32:01Z,6b4b7127544865b78f712d907e6f1719595f4232,Adela Almasan ssoSettingsApi,2023-11-08T09:50:01Z,2025-07-03T08:53:33Z,5285e9503be5702680acb2b52a6bda0632f4603d,Misi -logsInfiniteScrolling,2023-11-09T10:54:03Z,,174c2ab45a2af912519153c5c3e671f04396d7d7,Matias Chomicki flameGraphItemCollapsing,2023-11-09T14:31:07Z,2024-07-15T12:45:41Z,494a07b522df4e3ff9512b47767745a94c15f080,Andrej Ocenas alertingDetailsViewV2,2023-11-09T17:35:03Z,2024-03-14T14:18:01Z,323ee7c38ceb18b8e71c780d797fabac7041a673,Gilles De Mey alertingSimplifiedRouting,2023-11-10T13:14:39Z,2025-05-09T13:30:56Z,68e37c3925080cf64a5e7570eabb042f19ca2dbf,Sonia Aguilar diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 15d15a986a5..f05b0d41da3 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -56,6 +56,7 @@ kubernetesShortURLs,experimental,@grafana/grafana-app-platform-squad,false,true, useKubernetesShortURLsAPI,experimental,@grafana/sharing-squad,false,false,true kubernetesAlertingRules,experimental,@grafana/alerting-squad,false,true,false kubernetesCorrelations,experimental,@grafana/datapro,false,true,false +kubernetesUnifiedStorageQuotas,experimental,@grafana/search-and-storage,false,true,false kubernetesLogsDrilldown,experimental,@grafana/observability-logs,false,true,false kubernetesQueryCaching,experimental,@grafana/grafana-operator-experience-squad,false,true,false dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false @@ -223,6 +224,7 @@ kubernetesAuthzZanzanaSync,experimental,@grafana/identity-access-team,false,fals kubernetesAuthnMutation,experimental,@grafana/identity-access-team,false,false,false kubernetesExternalGroupMapping,experimental,@grafana/identity-access-team,false,false,false restoreDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,false +recentlyViewedDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,true alertEnrichment,experimental,@grafana/alerting-squad,false,false,false alertEnrichmentMultiStep,experimental,@grafana/alerting-squad,false,false,false alertEnrichmentConditional,experimental,@grafana/alerting-squad,false,false,false @@ -273,3 +275,4 @@ lokiQueryLimitsContext,experimental,@grafana/observability-logs,false,false,true rudderstackUpgrade,experimental,@grafana/grafana-frontend-platform,false,false,true kubernetesAlertingHistorian,experimental,@grafana/alerting-squad,false,true,false useMTPlugins,experimental,@grafana/plugins-platform-backend,false,false,true +multiPropsVariables,experimental,@grafana/dashboards-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index afc599d4eb8..6e106fd9950 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -183,6 +183,10 @@ const ( // Adds support for Kubernetes correlations FlagKubernetesCorrelations = "kubernetesCorrelations" + // FlagKubernetesUnifiedStorageQuotas + // Adds support for Kubernetes unified storage quotas + FlagKubernetesUnifiedStorageQuotas = "kubernetesUnifiedStorageQuotas" + // FlagKubernetesLogsDrilldown // Adds support for Kubernetes logs drilldown FlagKubernetesLogsDrilldown = "kubernetesLogsDrilldown" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 96ebe6dc5de..561ffd562ca 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2039,6 +2039,19 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "kubernetesUnifiedStorageQuotas", + "resourceVersion": "1764965198011", + "creationTimestamp": "2025-12-05T20:06:38Z" + }, + "spec": { + "description": "Adds support for Kubernetes unified storage quotas", + "stage": "experimental", + "codeowner": "@grafana/search-and-storage", + "requiresRestart": true + } + }, { "metadata": { "name": "localeFormatPreference", @@ -2259,6 +2272,19 @@ "codeowner": "@grafana/alerting-squad" } }, + { + "metadata": { + "name": "multiPropsVariables", + "resourceVersion": "1765293230587", + "creationTimestamp": "2025-12-09T15:13:50Z" + }, + "spec": { + "description": "Enables support for variables whose values can have multiple properties", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, { "metadata": { "name": "multiTenantTempCredentials", @@ -2907,6 +2933,23 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "recentlyViewedDashboards", + "resourceVersion": "1765227168175", + "creationTimestamp": "2025-12-08T20:44:44Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-12-08 20:52:48.175129 +0000 UTC" + } + }, + "spec": { + "description": "Enables recently viewed dashboards section in the browsing dashboard page", + "stage": "experimental", + "codeowner": "@grafana/grafana-search-navigate-organise", + "frontend": true, + "expression": "false" + } + }, { "metadata": { "name": "refactorVariablesTimeRange", diff --git a/pkg/services/ngalert/accesscontrol.go b/pkg/services/ngalert/accesscontrol.go index e36c123c621..15e3212de03 100644 --- a/pkg/services/ngalert/accesscontrol.go +++ b/pkg/services/ngalert/accesscontrol.go @@ -173,6 +173,7 @@ var ( Permissions: accesscontrol.ConcatPermissions(templatesReaderRole.Role.Permissions, []accesscontrol.Permission{ {Action: accesscontrol.ActionAlertingNotificationsTemplatesWrite}, {Action: accesscontrol.ActionAlertingNotificationsTemplatesDelete}, + {Action: accesscontrol.ActionAlertingNotificationsTemplatesTest}, }), }, } diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 46168688ee6..1c107db22c6 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -251,7 +251,7 @@ func (api *API) authorize(method, path string) web.Handler { case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/templates/test": eval = ac.EvalAny( ac.EvalPermission(ac.ActionAlertingNotificationsWrite), - ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesRead), + ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesTest), ) // External Alertmanager Paths diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index 3991df89e77..ebb3a1aa1e9 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -47,10 +47,10 @@ type Rule interface { Identifier() ngmodels.AlertRuleKeyWithGroup } -type ruleFactoryFunc func(context.Context, *ngmodels.AlertRule) Rule +type ruleFactoryFunc func(context.Context, ruleWithFolder) Rule -func (f ruleFactoryFunc) new(ctx context.Context, rule *ngmodels.AlertRule) Rule { - return f(ctx, rule) +func (f ruleFactoryFunc) new(ctx context.Context, rf ruleWithFolder) Rule { + return f(ctx, rf) } func newRuleFactory( @@ -70,11 +70,11 @@ func newRuleFactory( evalAppliedHook evalAppliedFunc, stopAppliedHook stopAppliedFunc, ) ruleFactoryFunc { - return func(ctx context.Context, rule *ngmodels.AlertRule) Rule { - if rule.Type() == ngmodels.RuleTypeRecording { + return func(ctx context.Context, rf ruleWithFolder) Rule { + if rf.rule.Type() == ngmodels.RuleTypeRecording { return newRecordingRule( ctx, - rule.GetKeyWithGroup(), + rf.rule.GetKeyWithGroup(), retryConfig, clock, evalFactory, @@ -89,7 +89,7 @@ func newRuleFactory( } return newAlertRule( ctx, - rule.GetKeyWithGroup(), + rf, appURL, disableGrafanaFolder, retryConfig, @@ -111,7 +111,8 @@ type evalAppliedFunc = func(ngmodels.AlertRuleKey, time.Time) type stopAppliedFunc = func(ngmodels.AlertRuleKey) type alertRule struct { - key ngmodels.AlertRuleKeyWithGroup + key ngmodels.AlertRuleKeyWithGroup + currentFingerprint fingerprint evalCh chan *Evaluation updateCh chan *Evaluation @@ -139,7 +140,7 @@ type alertRule struct { func newAlertRule( parent context.Context, - key ngmodels.AlertRuleKeyWithGroup, + rf ruleWithFolder, appURL *url.URL, disableGrafanaFolder bool, retryConfig RetryConfig, @@ -154,10 +155,13 @@ func newAlertRule( evalAppliedHook func(ngmodels.AlertRuleKey, time.Time), stopAppliedHook func(ngmodels.AlertRuleKey), ) *alertRule { + key := rf.rule.GetKeyWithGroup() + initialFingerprint := rf.Fingerprint() ctx, stop := util.WithCancelCause(ngmodels.WithRuleKey(parent, key.AlertRuleKey)) - return &alertRule{ + a := &alertRule{ key: key, + currentFingerprint: initialFingerprint, evalCh: make(chan *Evaluation), updateCh: make(chan *Evaluation), ctx: ctx, @@ -176,6 +180,8 @@ func newAlertRule( tracer: tracer, featureToggles: featureToggles, } + + return a } func (a *alertRule) Identifier() ngmodels.AlertRuleKeyWithGroup { @@ -246,22 +252,23 @@ func (a *alertRule) Run() error { grafanaCtx := a.ctx a.logger.Debug("Alert rule routine started") - var currentFingerprint fingerprint + firstEvalDone := false + defer a.stopApplied() for { select { // used by external services (API) to notify that rule is updated. case ctx := <-a.updateCh: fp := ctx.Fingerprint() - if currentFingerprint == fp { - a.logger.Info("Rule's fingerprint has not changed. Skip resetting the state", "currentFingerprint", currentFingerprint) + if a.currentFingerprint == fp { + a.logger.Info("Rule's fingerprint has not changed. Skip resetting the state", "currentFingerprint", a.currentFingerprint) continue } a.logger.Info("Clearing the state of the rule because it was updated", "isPaused", ctx.rule.IsPaused, "fingerprint", fp) // clear the state. So the next evaluation will start from the scratch. a.resetState(grafanaCtx, ctx.rule, ctx.rule.IsPaused) - currentFingerprint = fp + a.currentFingerprint = fp // evalCh - used by the scheduler to signal that evaluation is needed. case ctx, ok := <-a.evalCh: if !ok { @@ -295,20 +302,21 @@ func (a *alertRule) Run() error { for { isPaused := ctx.rule.IsPaused - // Do not clean up state if the eval loop has just started. var needReset bool - if currentFingerprint != 0 && currentFingerprint != f { - logger.Debug("Got a new version of alert rule. Clear up the state", "current_fingerprint", currentFingerprint, "fingerprint", f) + if a.currentFingerprint != f { + logger.Debug("Got a new version of alert rule. Clear up the state", "current_fingerprint", a.currentFingerprint, "fingerprint", f) needReset = true } // We need to reset state if the loop has started and the alert is already paused. It can happen, // if we have an alert with state and we do file provision with stateful Grafana, that state // lingers in DB and won't be cleaned up until next alert rule update. - needReset = needReset || (currentFingerprint == 0 && isPaused) + needReset = needReset || (!firstEvalDone && isPaused) if needReset { a.resetState(grafanaCtx, ctx.rule, isPaused) } - currentFingerprint = f + + firstEvalDone = true + a.currentFingerprint = f if isPaused { logger.Debug("Skip rule evaluation because it is paused") return @@ -319,7 +327,7 @@ func (a *alertRule) Run() error { evalTotal.Inc() } - fpStr := currentFingerprint.String() + fpStr := a.currentFingerprint.String() utcTick := ctx.scheduledAt.UTC().Format(time.RFC3339Nano) tracingCtx, span := a.tracer.Start(grafanaCtx, "alert rule execution", trace.WithAttributes( attribute.String("rule_uid", ctx.rule.UID), diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index 48be6bb4a40..6f4144b37bf 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -334,7 +334,7 @@ func TestAlertRuleAfterEval(t *testing.T) { ruleStore.PutRule(context.Background(), rule) ruleFactory := ruleFactoryFromScheduler(sch) - process := ruleFactory.new(context.Background(), rule) + process := ruleFactory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) return &testContext{ rule: rule, @@ -503,7 +503,14 @@ func blankRuleForTests(ctx context.Context, key models.AlertRuleKeyWithGroup) *a Log: log.NewNopLogger(), } st := state.NewManager(managerCfg, state.NewNoopPersister()) - return newAlertRule(ctx, key, nil, false, RetryConfig{}, nil, st, nil, nil, nil, log.NewNopLogger(), nil, featuremgmt.WithFeatures(), nil, nil) + // Create a minimal rule from the key + rule := &models.AlertRule{ + OrgID: key.OrgID, + UID: key.UID, + RuleGroup: key.RuleGroup, + } + rf := ruleWithFolder{rule: rule, folderTitle: ""} + return newAlertRule(ctx, rf, nil, false, RetryConfig{}, nil, st, nil, nil, nil, log.NewNopLogger(), nil, featuremgmt.WithFeatures(), nil, nil) } func TestRuleRoutine(t *testing.T) { @@ -540,7 +547,7 @@ func TestRuleRoutine(t *testing.T) { factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: folderTitle}) go func() { _ = ruleInfo.Run() }() @@ -728,7 +735,8 @@ func TestRuleRoutine(t *testing.T) { factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) - ruleInfo := factory.new(ctx, rule) + folderTitle := "" + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: folderTitle}) go func() { err := ruleInfo.Run() stoppedChan <- err @@ -750,7 +758,7 @@ func TestRuleRoutine(t *testing.T) { require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) factory := ruleFactoryFromScheduler(sch) - ruleInfo := factory.new(context.Background(), rule) + ruleInfo := factory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) go func() { err := ruleInfo.Run() stoppedChan <- err @@ -774,7 +782,7 @@ func TestRuleRoutine(t *testing.T) { require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) factory := ruleFactoryFromScheduler(sch) - ruleInfo := factory.new(context.Background(), rule) + ruleInfo := factory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) go func() { err := ruleInfo.Run() stoppedChan <- err @@ -804,7 +812,7 @@ func TestRuleRoutine(t *testing.T) { factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: folderTitle}) go func() { _ = ruleInfo.Run() @@ -871,6 +879,140 @@ func TestRuleRoutine(t *testing.T) { }) }) + t.Run("when update is sent before first evaluation", func(t *testing.T) { + rule := gen.With(withQueryForState(t, eval.Normal)).GenerateRef() + folderTitle := "folderName" + + evalAppliedChan := make(chan time.Time) + + sender := NewSyncAlertsSenderMock() + sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() + + sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender, clock.NewMock()) + ruleStore.PutRule(context.Background(), rule) + sch.schedulableAlertRules.set([]*models.AlertRule{rule}, map[models.FolderKey]string{rule.GetFolderKey(): folderTitle}) + + // Add state to verify it's not cleared + states := []*state.State{ + { + AlertRuleUID: rule.UID, + CacheID: data.Labels(rule.Labels).Fingerprint(), + OrgID: rule.OrgID, + State: eval.Alerting, + StartsAt: sch.clock.Now(), + EndsAt: sch.clock.Now().Add(5 * time.Second), + Labels: rule.Labels, + }, + } + sch.stateManager.Put(states) + + t.Run("should not reset state if fingerprint is the same", func(t *testing.T) { + factory := ruleFactoryFromScheduler(sch) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: folderTitle}) + + go func() { + _ = ruleInfo.Run() + }() + + // Send update before first evaluation - same rule, same fingerprint + // This should not reset state since fingerprint is the same + ruleInfo.Update(&Evaluation{rule: rule, folderTitle: folderTitle}) + + // Give time for update to be processed + time.Sleep(100 * time.Millisecond) + + actualStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + require.NotEmpty(t, actualStates) + }) + + t.Run("should reset state if fingerprint is different", func(t *testing.T) { + // Re-add state for this test + sch.stateManager.Put(states) + + sender := NewSyncAlertsSenderMock() + sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() + + sch2, ruleStore2, _, _ := createSchedule(make(chan time.Time), sender, clock.NewMock()) + ruleStore2.PutRule(context.Background(), rule) + sch2.schedulableAlertRules.set([]*models.AlertRule{rule}, map[models.FolderKey]string{rule.GetFolderKey(): folderTitle}) + sch2.stateManager.Put(states) + + factory := ruleFactoryFromScheduler(sch2) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: folderTitle}) + + go func() { + _ = ruleInfo.Run() + }() + + // Send update before first eval, with a changed alert rule title. + // This should reset state and send resolved alerts + updatedRule := models.CopyRule(rule, gen.WithTitle(util.GenerateShortUID())) + ruleInfo.Update(&Evaluation{rule: updatedRule, folderTitle: folderTitle}) + + // Wait for sender to be called (which happens when state is cleared and resolved alerts are sent) + require.Eventually(t, func() bool { + return len(sender.Calls()) > 0 + }, 5*time.Second, 100*time.Millisecond) + + // State should be cleared because fingerprint changed + actualStates := sch2.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + require.Empty(t, actualStates) + }) + }) + + t.Run("paused rule should reset state on first evaluation", func(t *testing.T) { + rule := gen.With(withQueryForState(t, eval.Normal)).GenerateRef() + rule.IsPaused = true + folderTitle := "folderName" + + sender := NewSyncAlertsSenderMock() + sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() + + sch, ruleStore, _, _ := createSchedule(make(chan time.Time), sender, clock.NewMock()) + ruleStore.PutRule(context.Background(), rule) + sch.schedulableAlertRules.set([]*models.AlertRule{rule}, map[models.FolderKey]string{rule.GetFolderKey(): folderTitle}) + + states := []*state.State{ + { + AlertRuleUID: rule.UID, + CacheID: data.Labels(rule.Labels).Fingerprint(), + OrgID: rule.OrgID, + State: eval.Alerting, + StartsAt: sch.clock.Now(), + EndsAt: sch.clock.Now().Add(5 * time.Second), + Labels: rule.Labels, + }, + } + sch.stateManager.Put(states) + require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) + + factory := ruleFactoryFromScheduler(sch) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: folderTitle}) + + go func() { + _ = ruleInfo.Run() + }() + + ruleInfo.Eval(&Evaluation{ + scheduledAt: sch.clock.Now(), + rule: rule, + folderTitle: folderTitle, + }) + + require.Eventually(t, func() bool { + return len(sender.Calls()) > 0 + }, 5*time.Second, 100*time.Millisecond) + + actualStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + require.Empty(t, actualStates) + }) + t.Run("when evaluation fails", func(t *testing.T) { rule := gen.With(withQueryForState(t, eval.Error)).GenerateRef() rule.ExecErrState = models.ErrorErrState @@ -910,7 +1052,7 @@ func TestRuleRoutine(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: ""}) go func() { _ = ruleInfo.Run() @@ -1044,7 +1186,7 @@ func TestRuleRoutine(t *testing.T) { factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: ""}) go func() { _ = ruleInfo.Run() @@ -1078,7 +1220,7 @@ func TestRuleRoutine(t *testing.T) { factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: ""}) go func() { _ = ruleInfo.Run() @@ -1119,7 +1261,7 @@ func TestRuleRoutine(t *testing.T) { factory := ruleFactoryFromScheduler(sch) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: ""}) go func() { _ = ruleInfo.Run() @@ -1214,7 +1356,7 @@ func TestAlertRuleRetry(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - ruleInfo := factory.new(ctx, rule) + ruleInfo := factory.new(ctx, ruleWithFolder{rule: rule, folderTitle: ""}) go func() { _ = ruleInfo.Run() diff --git a/pkg/services/ngalert/schedule/recording_rule_test.go b/pkg/services/ngalert/schedule/recording_rule_test.go index e8a61979d19..da25c38a2df 100644 --- a/pkg/services/ngalert/schedule/recording_rule_test.go +++ b/pkg/services/ngalert/schedule/recording_rule_test.go @@ -239,7 +239,7 @@ func TestRecordingRuleAfterEval(t *testing.T) { ruleStore.PutRule(context.Background(), rule) ruleFactory := ruleFactoryFromScheduler(sch) - process := ruleFactory.new(context.Background(), rule) + process := ruleFactory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) evalDoneChan := make(chan time.Time, 1) // Buffer to avoid blocking afterEvalCh := make(chan struct{}, 1) // Buffer to avoid blocking @@ -444,7 +444,7 @@ func testRecordingRule_Integration(t *testing.T, writeTarget *writer.TestRemoteW folderTitle := ruleStore.getNamespaceTitle(rule.NamespaceUID) ruleFactory := ruleFactoryFromScheduler(sch) - process := ruleFactory.new(context.Background(), rule) + process := ruleFactory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) evalDoneChan := make(chan time.Time) process.(*recordingRule).evalAppliedHook = func(_ models.AlertRuleKey, t time.Time) { evalDoneChan <- t @@ -583,7 +583,7 @@ func testRecordingRule_Integration(t *testing.T, writeTarget *writer.TestRemoteW folderTitle := ruleStore.getNamespaceTitle(rule.NamespaceUID) ruleFactory := ruleFactoryFromScheduler(sch) - process := ruleFactory.new(context.Background(), rule) + process := ruleFactory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) evalDoneChan := make(chan time.Time) process.(*recordingRule).evalAppliedHook = func(_ models.AlertRuleKey, t time.Time) { evalDoneChan <- t @@ -728,7 +728,7 @@ func testRecordingRule_Integration(t *testing.T, writeTarget *writer.TestRemoteW folderTitle := ruleStore.getNamespaceTitle(rule.NamespaceUID) ruleFactory := ruleFactoryFromScheduler(sch) - process := ruleFactory.new(context.Background(), rule) + process := ruleFactory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) evalDoneChan := make(chan time.Time) process.(*recordingRule).evalAppliedHook = func(_ models.AlertRuleKey, t time.Time) { evalDoneChan <- t @@ -787,7 +787,7 @@ func testRecordingRule_Integration(t *testing.T, writeTarget *writer.TestRemoteW folderTitle := ruleStore.getNamespaceTitle(rule.NamespaceUID) ruleFactory := ruleFactoryFromScheduler(sch) - process := ruleFactory.new(context.Background(), rule) + process := ruleFactory.new(context.Background(), ruleWithFolder{rule: rule, folderTitle: ""}) evalDoneChan := make(chan time.Time) process.(*recordingRule).evalAppliedHook = func(_ models.AlertRuleKey, t time.Time) { evalDoneChan <- t diff --git a/pkg/services/ngalert/schedule/registry.go b/pkg/services/ngalert/schedule/registry.go index ede71c94d75..892e0af8235 100644 --- a/pkg/services/ngalert/schedule/registry.go +++ b/pkg/services/ngalert/schedule/registry.go @@ -21,7 +21,7 @@ var ( ) type ruleFactory interface { - new(context.Context, *models.AlertRule) Rule + new(context.Context, ruleWithFolder) Rule } type ruleRegistry struct { @@ -35,14 +35,14 @@ func newRuleRegistry() ruleRegistry { // getOrCreate gets a rule routine from registry for the provided rule. If it does not exist, it creates a new one. // Returns a pointer to the rule routine and a flag that indicates whether it is a new struct or not. -func (r *ruleRegistry) getOrCreate(context context.Context, item *models.AlertRule, factory ruleFactory) (Rule, bool) { +func (r *ruleRegistry) getOrCreate(context context.Context, rf ruleWithFolder, factory ruleFactory) (Rule, bool) { r.mu.Lock() defer r.mu.Unlock() - key := item.GetKey() + key := rf.rule.GetKey() rule, ok := r.rules[key] if !ok { - rule = factory.new(context, item) + rule = factory.new(context, rf) r.rules[key] = rule } return rule, !ok diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index 44dced0cdea..ba12fe33461 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -320,10 +320,22 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. sch.stopAppliedFunc, ) for _, item := range alertRules { - ruleRoutine, newRoutine := sch.registry.getOrCreate(ctx, item, ruleFactory) key := item.GetKey() logger := sch.log.FromContext(ctx).New(key.LogContext()...) + var folderTitle string + if !sch.disableGrafanaFolder { + title, ok := folderTitles[item.GetFolderKey()] + if ok { + folderTitle = title + } else { + missingFolder[item.NamespaceUID] = append(missingFolder[item.NamespaceUID], item.UID) + } + } + + rf := ruleWithFolder{rule: item, folderTitle: folderTitle} + ruleRoutine, newRoutine := sch.registry.getOrCreate(ctx, rf, ruleFactory) + // enforce minimum evaluation interval if item.IntervalSeconds < int64(sch.minRuleInterval.Seconds()) { logger.Debug("Interval adjusted", "originalInterval", item.IntervalSeconds, "adjustedInterval", sch.minRuleInterval.Seconds()) @@ -337,7 +349,7 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. logger.Debug("Rule restarted because type changed", "old", ruleRoutine.Type(), "new", item.Type()) restartedRules = append(restartedRules, ruleRoutine) sch.registry.del(key) - ruleRoutine, newRoutine = sch.registry.getOrCreate(ctx, item, ruleFactory) + ruleRoutine, newRoutine = sch.registry.getOrCreate(ctx, rf, ruleFactory) } if newRoutine && !invalidInterval { @@ -357,16 +369,6 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. offset := jitterOffsetInTicks(item, sch.baseInterval, sch.jitterEvaluations) isReadyToRun := item.IntervalSeconds != 0 && (tickNum%itemFrequency)-offset == 0 - var folderTitle string - if !sch.disableGrafanaFolder { - title, ok := folderTitles[item.GetFolderKey()] - if ok { - folderTitle = title - } else { - missingFolder[item.NamespaceUID] = append(missingFolder[item.NamespaceUID], item.UID) - } - } - if isReadyToRun { logger.Debug("Rule is ready to run on the current tick", "tick", tick, "frequency", itemFrequency, "offset", offset) readyToRun = append(readyToRun, readyToRunItem{ruleRoutine: ruleRoutine, Evaluation: Evaluation{ @@ -378,12 +380,12 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. if _, isUpdated := updated[key]; isUpdated && !isReadyToRun { // if we do not need to eval the rule, check the whether rule was just updated and if it was, notify evaluation routine about that logger.Debug("Rule has been updated. Notifying evaluation routine") - go func(routine Rule, rule *ngmodels.AlertRule) { + go func(routine Rule, rule *ngmodels.AlertRule, folder string) { routine.Update(&Evaluation{ rule: rule, - folderTitle: folderTitle, + folderTitle: folder, }) - }(ruleRoutine, item) + }(ruleRoutine, item, folderTitle) updatedRules = append(updatedRules, ngmodels.AlertRuleKeyWithVersion{ Version: item.Version, AlertRuleKey: item.GetKey(), diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index c90c9b19707..fc402bcc793 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -1107,7 +1107,7 @@ func TestSchedule_deleteAlertRule(t *testing.T) { rule := models.RuleGen.GenerateRef() ruleStore.PutRule(ctx, rule) key := rule.GetKey() - info, _ := sch.registry.getOrCreate(ctx, rule, ruleFactory) + info, _ := sch.registry.getOrCreate(ctx, ruleWithFolder{rule: rule, folderTitle: ""}, ruleFactory) sch.deleteAlertRule(ctx, key) @@ -1126,7 +1126,7 @@ func TestSchedule_deleteAlertRule(t *testing.T) { rule := models.RuleGen.GenerateRef() ruleStore.PutRule(ctx, rule) key := rule.GetKey() - info, _ := sch.registry.getOrCreate(ctx, rule, ruleFactory) + info, _ := sch.registry.getOrCreate(ctx, ruleWithFolder{rule: rule, folderTitle: ""}, ruleFactory) _, err := sch.updateSchedulableAlertRules(ctx) require.NoError(t, err) @@ -1149,7 +1149,7 @@ func TestSchedule_deleteAlertRule(t *testing.T) { rule := models.RuleGen.GenerateRef() ruleStore.PutRule(ctx, rule) key := rule.GetKey() - info, _ := sch.registry.getOrCreate(ctx, rule, ruleFactory) + info, _ := sch.registry.getOrCreate(ctx, ruleWithFolder{rule: rule, folderTitle: ""}, ruleFactory) _, err := sch.updateSchedulableAlertRules(ctx) require.NoError(t, err) @@ -1172,7 +1172,7 @@ func TestSchedule_deleteAlertRule(t *testing.T) { ruleFactory := ruleFactoryFromScheduler(sch) rule := models.RuleGen.GenerateRef() key := rule.GetKey() - info, _ := sch.registry.getOrCreate(ctx, rule, ruleFactory) + info, _ := sch.registry.getOrCreate(ctx, ruleWithFolder{rule: rule, folderTitle: ""}, ruleFactory) _, err := sch.updateSchedulableAlertRules(ctx) require.NoError(t, err) diff --git a/pkg/services/ngalert/writer/datasourcewriter.go b/pkg/services/ngalert/writer/datasourcewriter.go index cc7036974e7..3cb082fb639 100644 --- a/pkg/services/ngalert/writer/datasourcewriter.go +++ b/pkg/services/ngalert/writer/datasourcewriter.go @@ -217,6 +217,8 @@ func (w *DatasourceWriter) makeWriter(ctx context.Context, orgID int64, dsUID st } for k, values := range dsHeaders { + // Clear out any custom writer headers before adding the data souce ones. + headers.Del(k) for _, v := range values { headers.Add(k, v) } diff --git a/pkg/services/ngalert/writer/datasourcewriter_test.go b/pkg/services/ngalert/writer/datasourcewriter_test.go index 349d8b9191a..04c86a954b3 100644 --- a/pkg/services/ngalert/writer/datasourcewriter_test.go +++ b/pkg/services/ngalert/writer/datasourcewriter_test.go @@ -141,7 +141,7 @@ func setupDataSources(t *testing.T) *testDataSources { res.DataSourceHeaders["prom-4"] = http.Header{ "X-Scope-OrgID": []string{"test-user"}, "X-Test-Header": []string{"test-value"}, - "X-Double-Header": []string{"one", "two", "three"}, + "X-Double-Header": []string{"one", "two"}, } return res @@ -231,7 +231,7 @@ func TestDatasourceWriter(t *testing.T) { cHeaders := map[string]string{ "X-Custom-Header": "test-value", "X-Another-Header": "another-value", - overwrittenHeader: "overwritten", // Data source headers should be overwritten by custom headers. + overwrittenHeader: "should-be-overwritten", // Data source headers overwrite custom headers. } cfg = DatasourceWriterConfig{ @@ -249,17 +249,20 @@ func TestDatasourceWriter(t *testing.T) { require.Len(t, dsHeaders, 3) // We're confirming we have a data source header with the same name but different value. - // This one should not be sent in the request. require.NotEmpty(t, dsHeaders[overwrittenHeader]) require.NotEqual(t, dsHeaders[overwrittenHeader], cHeaders[overwrittenHeader]) - // All headers (except for the one that was overwritten) should have been used. + // All data source headers should have been used. for k, vv := range dsHeaders { - if k != overwrittenHeader { - assert.Equal(t, vv, testDS.prom4.LastHeaders.Values(k)) - } + assert.Equal(t, vv, testDS.prom4.LastHeaders.Values(k)) } + + // All custom headers except for the overwritten one should have been used. for k, v := range cHeaders { + if k == overwrittenHeader { + assert.NotEqual(t, v, testDS.prom4.LastHeaders.Get(k)) + continue + } assert.Equal(t, v, testDS.prom4.LastHeaders.Get(k)) } }) diff --git a/pkg/services/updatemanager/plugins.go b/pkg/services/updatemanager/plugins.go index 79270f8a1ee..7ee11b261f9 100644 --- a/pkg/services/updatemanager/plugins.go +++ b/pkg/services/updatemanager/plugins.go @@ -13,6 +13,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/open-feature/go-sdk/openfeature" "go.opentelemetry.io/otel/codes" "github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider" @@ -96,11 +97,7 @@ func (s *PluginsService) IsDisabled() bool { } func (s *PluginsService) Run(ctx context.Context) error { - s.instrumentedCheckForUpdates(ctx) - //nolint:staticcheck // not yet migrated to OpenFeature - if s.features.IsEnabledGlobally(featuremgmt.FlagPluginsAutoUpdate) { - s.updateAll(ctx) - } + s.checkAndUpdate(ctx) ticker := time.NewTicker(time.Minute * 10) run := true @@ -108,11 +105,7 @@ func (s *PluginsService) Run(ctx context.Context) error { for run { select { case <-ticker.C: - s.instrumentedCheckForUpdates(ctx) - //nolint:staticcheck // not yet migrated to OpenFeature - if s.features.IsEnabledGlobally(featuremgmt.FlagPluginsAutoUpdate) { - s.updateAll(ctx) - } + s.checkAndUpdate(ctx) case <-ctx.Done(): run = false } @@ -140,6 +133,14 @@ func (s *PluginsService) HasUpdate(ctx context.Context, pluginID string) (string return "", false } +// checkAndUpdate checks for updates and applies them if auto-update is enabled. +func (s *PluginsService) checkAndUpdate(ctx context.Context) { + s.instrumentedCheckForUpdates(ctx) + if openfeature.NewDefaultClient().Boolean(ctx, featuremgmt.FlagPluginsAutoUpdate, false, openfeature.TransactionContext(ctx)) { + s.updateAll(ctx) + } +} + func (s *PluginsService) instrumentedCheckForUpdates(ctx context.Context) { start := time.Now() ctx, span := s.tracer.Start(ctx, "updatechecker.PluginsService.checkForUpdates") @@ -226,8 +227,7 @@ func (s *PluginsService) canUpdate(ctx context.Context, plugin pluginstore.Plugi return false } - //nolint:staticcheck // not yet migrated to OpenFeature - if s.features.IsEnabledGlobally(featuremgmt.FlagPluginsAutoUpdate) { + if openfeature.NewDefaultClient().Boolean(ctx, featuremgmt.FlagPluginsAutoUpdate, false, openfeature.TransactionContext(ctx)) { return s.updateChecker.CanUpdate(plugin.ID, plugin.Info.Version, gcomVersion, s.updateStrategy == setting.PluginUpdateStrategyMinor) } diff --git a/pkg/services/updatemanager/plugins_test.go b/pkg/services/updatemanager/plugins_test.go index 0455963f168..75834c44c7d 100644 --- a/pkg/services/updatemanager/plugins_test.go +++ b/pkg/services/updatemanager/plugins_test.go @@ -6,8 +6,10 @@ import ( "net/http" "net/url" "strings" + "sync" "testing" + "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" @@ -19,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins" + "github.com/grafana/grafana/pkg/setting" ) type mockPluginPreinstall struct { @@ -240,6 +243,7 @@ func TestPluginUpdateChecker_checkForUpdates(t *testing.T) { require.Empty(t, svc.availableUpdates["test-core-panel"]) }) } + func TestPluginUpdateChecker_updateAll(t *testing.T) { t.Run("update is available", func(t *testing.T) { pluginsFakeStore := map[string]string{} @@ -300,3 +304,88 @@ func (c *fakeHTTPClient) Do(req *http.Request) (*http.Response, error) { return resp, nil } + +func TestPluginsService_PluginsAutoUpdateFlag(t *testing.T) { + updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck") + + tests := []struct { + name string + flagEnabled bool + expectUpdate bool + }{ + { + name: "pluginsAutoUpdate enabled calls updateAll", + flagEnabled: true, + expectUpdate: true, + }, + { + name: "pluginsAutoUpdate disabled does not call updateAll", + flagEnabled: false, + expectUpdate: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setupOpenFeatureProvider(t, tt.flagEnabled) + + updateCallCount := 0 + + availableUpdates := map[string]availableUpdate{ + "test-plugin": { + localVersion: "0.9.0", + availableVersion: "1.0.0", + }, + } + + svc := &PluginsService{ + availableUpdates: availableUpdates, + httpClient: &fakeHTTPClient{ + fakeResp: `[]`, + }, + log: log.NewNopLogger(), + tracer: tracing.InitializeTracerForTest(), + updateCheckURL: updateCheckURL, + updateChecker: pluginchecker.ProvideService(managedplugins.NewNoop(), provisionedplugins.NewNoop(), &mockPluginPreinstall{}), + pluginStore: &pluginstore.FakePluginStore{PluginList: []pluginstore.Plugin{}}, + pluginInstaller: &pluginfakes.FakePluginInstaller{ + AddFunc: func(ctx context.Context, pluginID, version string, opts plugins.AddOpts) error { + updateCallCount++ + return nil + }, + }, + grafanaVersion: "10.0.0", + } + + ctx := context.Background() + // Test the synchronous initialization work directly, without the long-running ticker loop + svc.checkAndUpdate(ctx) + + if tt.expectUpdate { + require.Equal(t, updateCallCount, 1, "updateAll should be called when flag is enabled") + } else { + require.Equal(t, 0, updateCallCount, "updateAll should not be called when flag is disabled") + } + }) + } +} + +var openfeatureTestMutex sync.Mutex + +func setupOpenFeatureProvider(t *testing.T, flagValue bool) { + t.Helper() + openfeatureTestMutex.Lock() + + err := featuremgmt.InitOpenFeature(featuremgmt.OpenFeatureConfig{ + ProviderType: setting.StaticProviderType, + StaticFlags: map[string]bool{ + featuremgmt.FlagPluginsAutoUpdate: flagValue, + }, + }) + require.NoError(t, err) + + t.Cleanup(func() { + _ = openfeature.SetProviderAndWait(openfeature.NoopProvider{}) + openfeatureTestMutex.Unlock() + }) +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index f6c0b3d3f19..a01549d9da5 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -247,11 +247,12 @@ type Cfg struct { MetricsGrafanaEnvironmentInfo map[string]string // Dashboards - DashboardVersionsToKeep int - MinRefreshInterval string - DefaultHomeDashboardPath string - DashboardPerformanceMetrics []string - PanelSeriesLimit int + DashboardVersionsToKeep int + MinRefreshInterval string + DefaultHomeDashboardPath string + DashboardPerformanceMetrics []string + PanelSeriesLimit int + DashboardSchemaMigrationCacheTTL time.Duration // Auth LoginCookieName string @@ -1237,6 +1238,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.DefaultHomeDashboardPath = dashboards.Key("default_home_dashboard_path").MustString("") cfg.DashboardPerformanceMetrics = util.SplitString(dashboards.Key("dashboard_performance_metrics").MustString("")) cfg.PanelSeriesLimit = dashboards.Key("panel_series_limit").MustInt(0) + cfg.DashboardSchemaMigrationCacheTTL = dashboards.Key("schema_migration_cache_ttl").MustDuration(time.Minute) if err := readUserSettings(iniFile, cfg); err != nil { return err @@ -1330,7 +1332,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.QueryHistoryEnabled = queryHistory.Key("enabled").MustBool(true) shortLinks := iniFile.Section("short_links") - cfg.ShortLinkExpiration = shortLinks.Key("expire_time").MustInt(7) + cfg.ShortLinkExpiration = shortLinks.Key("expire_time").MustInt(-1) if cfg.ShortLinkExpiration > 365 { cfg.Logger.Warn("short_links expire_time must be less than 366 days. Setting to 365 days") diff --git a/pkg/storage/unified/README.md b/pkg/storage/unified/README.md index 7131178e8a1..e8cf6598d19 100644 --- a/pkg/storage/unified/README.md +++ b/pkg/storage/unified/README.md @@ -280,6 +280,35 @@ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest - make changes in `.proto` file - to compile all protobuf files in the repository run `make protobuf` at its top level +## Enable Quotas/Overrides +Quotas will make unified storage impose resource limits on a namespace. By default, the limit is 1000, but it can be overridden. To enable, create an empty overrides.yaml file in the grafana root directory. + +Then add the following to your grafana ini: +```ini +[feature_toggles] +kubernetesUnifiedStorageQuotas = true + +[unified_storage] +overrides_path = overrides.yaml +overrides_reload_period = 5s +``` + +To overrides the default quota for a tenant, add the following to the overrides.yaml file: +```yaml +overrides: + : + quotas: + .: + limit: 10 +``` +Unless otherwise set, the NAMESPACE when running locally is `default`. + +To access quotas, use the following API endpoint: +``` +GET /apis/quotas.grafana.app/v0alpha1/namespaces//usage?group=&resource= +``` + + ## Setting up search To enable it, add the following to your `custom.ini` under the `[feature_toggles]` and `[unified_storage]` sections: ```ini diff --git a/pkg/storage/unified/apistore/store_test.go b/pkg/storage/unified/apistore/store_test.go index 495efc32175..5d7c5a372a0 100644 --- a/pkg/storage/unified/apistore/store_test.go +++ b/pkg/storage/unified/apistore/store_test.go @@ -166,6 +166,7 @@ type resourceClientMock struct { resourcepb.BulkStoreClient resourcepb.BlobStoreClient resourcepb.DiagnosticsClient + resourcepb.QuotasClient } // always return GRPC Unauthenticated code diff --git a/pkg/storage/unified/proto/resource.proto b/pkg/storage/unified/proto/resource.proto index 91a9288b194..ca91d9efd21 100644 --- a/pkg/storage/unified/proto/resource.proto +++ b/pkg/storage/unified/proto/resource.proto @@ -587,6 +587,22 @@ message ResourceTableRow { bytes object = 4; } +message QuotaUsageRequest { + // Namespace (tenant) + ResourceKey key = 1; +} + +message QuotaUsageResponse { + // Error details + ErrorResult error = 1; + + // Current usage + int64 usage = 2; + + // Current limit + int64 limit = 3; +} + // This provides the CRUD+List+Watch support needed for a k8s apiserver // The semantics and behaviors of this service are constrained by kubernetes // This does not understand the resource schemas, only deals with json bytes @@ -631,3 +647,8 @@ service Diagnostics { // Check if the service is healthy rpc IsHealthy(HealthCheckRequest) returns (HealthCheckResponse); } + +service Quotas { + // Get current quota usage and limits + rpc GetQuotaUsage(QuotaUsageRequest) returns (QuotaUsageResponse); +} diff --git a/pkg/storage/unified/resource/client.go b/pkg/storage/unified/resource/client.go index b2742f71dc9..e51b7ec4876 100644 --- a/pkg/storage/unified/resource/client.go +++ b/pkg/storage/unified/resource/client.go @@ -39,6 +39,7 @@ type ResourceClient interface { resourcepb.BulkStoreClient resourcepb.BlobStoreClient resourcepb.DiagnosticsClient + resourcepb.QuotasClient } // Internal implementation @@ -49,6 +50,7 @@ type resourceClient struct { resourcepb.BulkStoreClient resourcepb.BlobStoreClient resourcepb.DiagnosticsClient + resourcepb.QuotasClient } func NewResourceClient(conn, indexConn grpc.ClientConnInterface, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer trace.Tracer) (ResourceClient, error) { @@ -76,6 +78,7 @@ func newResourceClient(storageCc grpc.ClientConnInterface, indexCc grpc.ClientCo BulkStoreClient: resourcepb.NewBulkStoreClient(storageCc), BlobStoreClient: resourcepb.NewBlobStoreClient(storageCc), DiagnosticsClient: resourcepb.NewDiagnosticsClient(storageCc), + QuotasClient: resourcepb.NewQuotasClient(storageCc), } } @@ -102,6 +105,7 @@ func NewLocalResourceClient(server ResourceServer) ResourceClient { &resourcepb.BlobStore_ServiceDesc, &resourcepb.BulkStore_ServiceDesc, &resourcepb.Diagnostics_ServiceDesc, + &resourcepb.Quotas_ServiceDesc, } { channel.RegisterService( grpchan.InterceptServer( diff --git a/pkg/storage/unified/resource/client_mock.go b/pkg/storage/unified/resource/client_mock.go index fcc7392880e..059421b487a 100644 --- a/pkg/storage/unified/resource/client_mock.go +++ b/pkg/storage/unified/resource/client_mock.go @@ -394,6 +394,80 @@ func (_c *MockResourceClient_GetBlob_Call) RunAndReturn(run func(context.Context return _c } +// GetQuotaUsage provides a mock function with given fields: ctx, in, opts +func (_m *MockResourceClient) GetQuotaUsage(ctx context.Context, in *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetQuotaUsage") + } + + var r0 *resourcepb.QuotaUsageResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) *resourcepb.QuotaUsageResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*resourcepb.QuotaUsageResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockResourceClient_GetQuotaUsage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetQuotaUsage' +type MockResourceClient_GetQuotaUsage_Call struct { + *mock.Call +} + +// GetQuotaUsage is a helper method to define mock.On call +// - ctx context.Context +// - in *resourcepb.QuotaUsageRequest +// - opts ...grpc.CallOption +func (_e *MockResourceClient_Expecter) GetQuotaUsage(ctx interface{}, in interface{}, opts ...interface{}) *MockResourceClient_GetQuotaUsage_Call { + return &MockResourceClient_GetQuotaUsage_Call{Call: _e.mock.On("GetQuotaUsage", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *MockResourceClient_GetQuotaUsage_Call) Run(run func(ctx context.Context, in *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption)) *MockResourceClient_GetQuotaUsage_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]grpc.CallOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(grpc.CallOption) + } + } + run(args[0].(context.Context), args[1].(*resourcepb.QuotaUsageRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockResourceClient_GetQuotaUsage_Call) Return(_a0 *resourcepb.QuotaUsageResponse, _a1 error) *MockResourceClient_GetQuotaUsage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockResourceClient_GetQuotaUsage_Call) RunAndReturn(run func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error)) *MockResourceClient_GetQuotaUsage_Call { + _c.Call.Return(run) + return _c +} + // GetStats provides a mock function with given fields: ctx, in, opts func (_m *MockResourceClient) GetStats(ctx context.Context, in *resourcepb.ResourceStatsRequest, opts ...grpc.CallOption) (*resourcepb.ResourceStatsResponse, error) { _va := make([]interface{}, len(opts)) diff --git a/pkg/storage/unified/resource/fieldSelector.go b/pkg/storage/unified/resource/fieldSelector.go new file mode 100644 index 00000000000..034085ca07f --- /dev/null +++ b/pkg/storage/unified/resource/fieldSelector.go @@ -0,0 +1,53 @@ +package resource + +import ( + "context" + + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" +) + +// Some list queries can be calculated with simple reads or search index +func (s *server) tryFieldSelector(ctx context.Context, req *resourcepb.ListRequest) *resourcepb.ListResponse { + if req.Source != resourcepb.ListRequest_STORE || req.Options.Key.Namespace == "" { + return nil + } + + var names []string + for _, v := range req.Options.Fields { + if v.Key == "metadata.name" && v.Operator == `=` { + names = v.Values + continue + } + + // TODO: support other field selectors + } + + // The required names + if len(names) > 0 { + read := &resourcepb.ReadRequest{ + Key: req.Options.Key, + ResourceVersion: req.ResourceVersion, + } + rsp := &resourcepb.ListResponse{ + ResourceVersion: 1, // TODO, search result should include when it was indexed + } + for _, name := range names { + read.Key.Name = name + found, err := s.Read(ctx, read) + if err != nil { + return &resourcepb.ListResponse{Error: AsErrorResult(err)} + } + if len(found.Value) > 0 { + rsp.Items = append(rsp.Items, &resourcepb.ResourceWrapper{ + Value: found.Value, + ResourceVersion: found.ResourceVersion, + }) + if found.ResourceVersion > rsp.ResourceVersion { + rsp.ResourceVersion = found.ResourceVersion + } + } + } + return rsp + } + return nil +} diff --git a/pkg/storage/unified/resource/quotas.go b/pkg/storage/unified/resource/quotas.go index d956a6da017..03fe515559e 100644 --- a/pkg/storage/unified/resource/quotas.go +++ b/pkg/storage/unified/resource/quotas.go @@ -47,13 +47,14 @@ type Overrides struct { /* This service loads overrides (currently just quotas) from a YAML file with the following yaml structure: -"123": +overrides: + "123": quotas: - grafana.dashboard.app/dashboards: - limit: 1500 - grafana.folder.app/folders: - limit: 1500 + dashboard.grafana.app/dashboards: + limit: 1500 + folder.grafana.app/folders: + limit: 1500 */ func NewOverridesService(_ context.Context, logger log.Logger, reg prometheus.Registerer, tracer trace.Tracer, opts ReloadOptions) (*OverridesService, error) { // shouldn't be empty since we use file path existence to determine if we should enable the service @@ -76,12 +77,14 @@ func NewOverridesService(_ context.Context, logger log.Logger, reg prometheus.Re ReloadPeriod: opts.ReloadPeriod, LoadPath: []string{opts.FilePath}, Loader: func(r io.Reader) (interface{}, error) { - var tenants map[string]NamespaceOverrides + var raw struct { + Overrides map[string]NamespaceOverrides `yaml:"overrides"` + } decoder := yaml.NewDecoder(r) - if err := decoder.Decode(&tenants); err != nil { + if err := decoder.Decode(&raw); err != nil { return nil, err } - return &Overrides{Namespaces: tenants}, nil + return &Overrides{Namespaces: raw.Overrides}, nil }, } diff --git a/pkg/storage/unified/resource/quotas_test.go b/pkg/storage/unified/resource/quotas_test.go index 97f9f355af6..097a02d7444 100644 --- a/pkg/storage/unified/resource/quotas_test.go +++ b/pkg/storage/unified/resource/quotas_test.go @@ -27,10 +27,11 @@ func TestNewQuotaService(t *testing.T) { opts: ReloadOptions{}, setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -105,10 +106,11 @@ func TestQuotaService_ConfigReload(t *testing.T) { // Create a temporary config file tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - initialConfig := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 + initialConfig := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(initialConfig), 0644)) @@ -139,14 +141,15 @@ func TestQuotaService_ConfigReload(t *testing.T) { assert.Equal(t, 1500, quota.Limit, "initial quota should be 1500") // Update the config file with new values - updatedConfig := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 2500 -"456": - quotas: - grafana.folder.app/folders: - limit: 3000 + updatedConfig := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 2500 + "456": + quotas: + grafana.folder.app/folders: + limit: 3000 ` require.NoError(t, os.WriteFile(tmpFile, []byte(updatedConfig), 0644)) @@ -183,10 +186,11 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns custom quota for matching tenant and resource", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -204,10 +208,11 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns default quota when tenant not found", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -225,10 +230,11 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns default quota when resource not found", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -246,10 +252,11 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "handles namespace without stacks- prefix", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -284,12 +291,13 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "handles multiple resources for same tenant", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 - grafana.folder.app/folders: - limit: 2500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -307,12 +315,13 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns error when namespace is empty", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 - grafana.folder.app/folders: - limit: 2500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -330,12 +339,13 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns error when group is empty", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 - grafana.folder.app/folders: - limit: 2500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -353,12 +363,13 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns error when resource is empty", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 - grafana.folder.app/folders: - limit: 2500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile @@ -376,12 +387,13 @@ func TestQuotaService_GetQuota(t *testing.T) { name: "returns error when all fields are empty", setupFile: func(t *testing.T) string { tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") - content := `"123": - quotas: - grafana.dashboard.app/dashboards: - limit: 1500 - grafana.folder.app/folders: - limit: 2500 + content := `overrides: + "123": + quotas: + grafana.dashboard.app/dashboards: + limit: 1500 + grafana.folder.app/folders: + limit: 2500 ` require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) return tmpFile diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 4761eb10def..62523e6550e 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -40,6 +40,7 @@ type ResourceServer interface { resourcepb.ManagedObjectIndexServer resourcepb.BlobStoreServer resourcepb.DiagnosticsServer + resourcepb.QuotasServer } type ListIterator interface { @@ -1431,6 +1432,38 @@ func (s *server) PutBlob(ctx context.Context, req *resourcepb.PutBlobRequest) (* return rsp, nil } +func (s *server) GetQuotaUsage(ctx context.Context, req *resourcepb.QuotaUsageRequest) (*resourcepb.QuotaUsageResponse, error) { + if s.overridesService == nil { + return &resourcepb.QuotaUsageResponse{Error: &resourcepb.ErrorResult{ + Message: "overrides service not configured on resource server", + Code: http.StatusNotImplemented, + }}, nil + } + nsr := NamespacedResource{ + Namespace: req.Key.Namespace, + Group: req.Key.Group, + Resource: req.Key.Resource, + } + usage, err := s.backend.GetResourceStats(ctx, nsr, 0) + if err != nil { + return &resourcepb.QuotaUsageResponse{Error: AsErrorResult(err)}, nil + } + limit, err := s.overridesService.GetQuota(ctx, nsr) + if err != nil { + return &resourcepb.QuotaUsageResponse{Error: AsErrorResult(err)}, nil + } + + // handle case where no resources exist yet - very unlikely but possible + rsp := &resourcepb.QuotaUsageResponse{Limit: int64(limit.Limit)} + if len(usage) <= 0 { + rsp.Usage = 0 + } else { + rsp.Usage = usage[0].Count + } + + return rsp, nil +} + func (s *server) getPartialObject(ctx context.Context, key *resourcepb.ResourceKey, rv int64) (utils.GrafanaMetaAccessor, *resourcepb.ErrorResult) { if r := verifyRequestKey(key); r != nil { return nil, r @@ -1550,6 +1583,7 @@ func (s *server) checkQuota(ctx context.Context, nsr NamespacedResource) { )) if s.overridesService == nil { + s.log.FromContext(ctx).Debug("overrides service not configured, skipping quota check", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource) return } @@ -1564,6 +1598,12 @@ func (s *server) checkQuota(ctx context.Context, nsr NamespacedResource) { s.log.FromContext(ctx).Error("failed to get resource stats for quota checking", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "error", err) return } + if len(stats) > 0 { + s.log.FromContext(ctx).Debug("stats found", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "count", stats[0].Count) + } else { + s.log.FromContext(ctx).Debug("no stats found for resource", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource) + } + if len(stats) > 0 && stats[0].Count >= int64(quota.Limit) { s.log.FromContext(ctx).Info("Quota exceeded on create", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "quota", quota.Limit, "count", stats[0].Count, "stats_resource", stats[0].Resource) } diff --git a/pkg/storage/unified/resource/server_test.go b/pkg/storage/unified/resource/server_test.go index 504c49616f9..0b0bbcfba13 100644 --- a/pkg/storage/unified/resource/server_test.go +++ b/pkg/storage/unified/resource/server_test.go @@ -5,6 +5,8 @@ import ( "encoding/json" "errors" "net/http" + "os" + "path/filepath" "strings" "sync" "testing" @@ -22,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/util/scheduler" ) @@ -614,3 +617,71 @@ func TestArtificialDelayAfterSuccessfulOperation(t *testing.T) { check(t, false, &resourcepb.UpdateResponse{Error: AsErrorResult(errors.New("some error"))}, nil) check(t, false, &resourcepb.DeleteResponse{Error: AsErrorResult(errors.New("some error"))}, nil) } + +func TestGetQuotaUsage(t *testing.T) { + ctx := context.Background() + + t.Run("returns error when overrides service is not configured", func(t *testing.T) { + s := &server{ + overridesService: nil, + log: log.NewNopLogger(), + } + + resp, err := s.GetQuotaUsage(ctx, &resourcepb.QuotaUsageRequest{ + Key: &resourcepb.ResourceKey{ + Namespace: "stacks-123", + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + }) + require.NoError(t, err) + require.NotNil(t, resp.Error) + assert.Equal(t, int32(http.StatusNotImplemented), resp.Error.Code) + assert.Equal(t, "overrides service not configured on resource server", resp.Error.Message) + }) + + t.Run("returns usage and limit successfully", func(t *testing.T) { + // Create a temporary overrides config file + tmpFile := filepath.Join(t.TempDir(), "overrides.yaml") + content := `overrides: + "123": + quotas: + dashboard.grafana.app/dashboards: + limit: 500 +` + require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644)) + + // Create a real OverridesService with the temp file + overridesService, err := NewOverridesService(ctx, log.NewNopLogger(), prometheus.NewRegistry(), tracing.NewNoopTracerService(), ReloadOptions{ + FilePath: tmpFile, + }) + require.NoError(t, err) + require.NoError(t, overridesService.init(ctx)) + defer func() { + _ = overridesService.stop(ctx) + }() + + // Create a mock backend that returns resource stats (reusing mockStorageBackend from search_test.go) + mockBackend := &mockStorageBackend{ + resourceStats: []ResourceStats{{Count: 42}}, + } + + s := &server{ + backend: mockBackend, + overridesService: overridesService, + log: log.NewNopLogger(), + } + + resp, err := s.GetQuotaUsage(ctx, &resourcepb.QuotaUsageRequest{ + Key: &resourcepb.ResourceKey{ + Namespace: "stacks-123", + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + }) + require.NoError(t, err) + require.Nil(t, resp.Error) + assert.Equal(t, int64(42), resp.Usage) + assert.Equal(t, int64(500), resp.Limit) + }) +} diff --git a/pkg/storage/unified/resourcepb/resource.pb.go b/pkg/storage/unified/resourcepb/resource.pb.go index 8c6fb47b491..8046b354ccd 100644 --- a/pkg/storage/unified/resourcepb/resource.pb.go +++ b/pkg/storage/unified/resourcepb/resource.pb.go @@ -2484,6 +2484,114 @@ func (x *ResourceTableRow) GetObject() []byte { return nil } +type QuotaUsageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Namespace (tenant) + Key *ResourceKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QuotaUsageRequest) Reset() { + *x = QuotaUsageRequest{} + mi := &file_resource_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QuotaUsageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QuotaUsageRequest) ProtoMessage() {} + +func (x *QuotaUsageRequest) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QuotaUsageRequest.ProtoReflect.Descriptor instead. +func (*QuotaUsageRequest) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{30} +} + +func (x *QuotaUsageRequest) GetKey() *ResourceKey { + if x != nil { + return x.Key + } + return nil +} + +type QuotaUsageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Error details + Error *ErrorResult `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + // Current usage + Usage int64 `protobuf:"varint,2,opt,name=usage,proto3" json:"usage,omitempty"` + // Current limit + Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QuotaUsageResponse) Reset() { + *x = QuotaUsageResponse{} + mi := &file_resource_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QuotaUsageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QuotaUsageResponse) ProtoMessage() {} + +func (x *QuotaUsageResponse) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QuotaUsageResponse.ProtoReflect.Descriptor instead. +func (*QuotaUsageResponse) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{31} +} + +func (x *QuotaUsageResponse) GetError() *ErrorResult { + if x != nil { + return x.Error + } + return nil +} + +func (x *QuotaUsageResponse) GetUsage() int64 { + if x != nil { + return x.Usage + } + return 0 +} + +func (x *QuotaUsageResponse) GetLimit() int64 { + if x != nil { + return x.Limit + } + return 0 +} + type WatchEvent_Resource struct { state protoimpl.MessageState `protogen:"open.v1"` Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` @@ -2494,7 +2602,7 @@ type WatchEvent_Resource struct { func (x *WatchEvent_Resource) Reset() { *x = WatchEvent_Resource{} - mi := &file_resource_proto_msgTypes[30] + mi := &file_resource_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2506,7 +2614,7 @@ func (x *WatchEvent_Resource) String() string { func (*WatchEvent_Resource) ProtoMessage() {} func (x *WatchEvent_Resource) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[30] + mi := &file_resource_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2553,7 +2661,7 @@ type BulkResponse_Summary struct { func (x *BulkResponse_Summary) Reset() { *x = BulkResponse_Summary{} - mi := &file_resource_proto_msgTypes[31] + mi := &file_resource_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2565,7 +2673,7 @@ func (x *BulkResponse_Summary) String() string { func (*BulkResponse_Summary) ProtoMessage() {} func (x *BulkResponse_Summary) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[31] + mi := &file_resource_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2649,7 +2757,7 @@ type BulkResponse_Rejected struct { func (x *BulkResponse_Rejected) Reset() { *x = BulkResponse_Rejected{} - mi := &file_resource_proto_msgTypes[32] + mi := &file_resource_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2661,7 +2769,7 @@ func (x *BulkResponse_Rejected) String() string { func (*BulkResponse_Rejected) ProtoMessage() {} func (x *BulkResponse_Rejected) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[32] + mi := &file_resource_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2718,7 +2826,7 @@ type ListManagedObjectsResponse_Item struct { func (x *ListManagedObjectsResponse_Item) Reset() { *x = ListManagedObjectsResponse_Item{} - mi := &file_resource_proto_msgTypes[33] + mi := &file_resource_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2730,7 +2838,7 @@ func (x *ListManagedObjectsResponse_Item) String() string { func (*ListManagedObjectsResponse_Item) ProtoMessage() {} func (x *ListManagedObjectsResponse_Item) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[33] + mi := &file_resource_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2801,7 +2909,7 @@ type CountManagedObjectsResponse_ResourceCount struct { func (x *CountManagedObjectsResponse_ResourceCount) Reset() { *x = CountManagedObjectsResponse_ResourceCount{} - mi := &file_resource_proto_msgTypes[34] + mi := &file_resource_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2813,7 +2921,7 @@ func (x *CountManagedObjectsResponse_ResourceCount) String() string { func (*CountManagedObjectsResponse_ResourceCount) ProtoMessage() {} func (x *CountManagedObjectsResponse_ResourceCount) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[34] + mi := &file_resource_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2886,7 +2994,7 @@ type ResourceTableColumnDefinition_Properties struct { func (x *ResourceTableColumnDefinition_Properties) Reset() { *x = ResourceTableColumnDefinition_Properties{} - mi := &file_resource_proto_msgTypes[35] + mi := &file_resource_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2898,7 +3006,7 @@ func (x *ResourceTableColumnDefinition_Properties) String() string { func (*ResourceTableColumnDefinition_Properties) ProtoMessage() {} func (x *ResourceTableColumnDefinition_Properties) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[35] + mi := &file_resource_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3320,68 +3428,85 @@ var file_resource_proto_rawDesc = string([]byte{ 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2a, 0x49, - 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, - 0x41, 0x54, 0x45, 0x44, 0x5f, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, - 0x6e, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, 0x41, 0x54, 0x45, - 0x44, 0x5f, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x2a, 0x4d, 0x0a, 0x16, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, - 0x68, 0x56, 0x32, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, - 0x12, 0x09, 0x0a, 0x05, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, - 0x78, 0x61, 0x63, 0x74, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, - 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x03, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, - 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, - 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, - 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, - 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0x4b, 0x0a, 0x09, 0x42, 0x75, 0x6c, 0x6b, - 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x42, 0x75, 0x6c, 0x6b, 0x50, 0x72, 0x6f, - 0x63, 0x65, 0x73, 0x73, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x28, 0x01, 0x32, 0xd9, 0x01, 0x0a, 0x12, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x62, 0x0a, 0x13, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x73, 0x12, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x5f, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x23, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x32, 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, - 0x12, 0x48, 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, - 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, - 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, - 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x3c, + 0x0a, 0x11, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x6d, 0x0a, 0x12, + 0x51, 0x75, 0x6f, 0x74, 0x61, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, + 0x14, 0x0a, 0x05, 0x75, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, + 0x75, 0x73, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2a, 0x49, 0x0a, 0x14, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, + 0x74, 0x63, 0x68, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, 0x41, 0x54, 0x45, + 0x44, 0x5f, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, + 0x12, 0x14, 0x0a, 0x10, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x45, + 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x2a, 0x4d, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x56, 0x32, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, + 0x05, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, + 0x74, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, + 0x68, 0x61, 0x6e, 0x10, 0x03, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, + 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, + 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05, + 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0x4b, 0x0a, 0x09, 0x42, 0x75, 0x6c, 0x6b, 0x53, 0x74, 0x6f, + 0x72, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x42, 0x75, 0x6c, 0x6b, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, + 0x73, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, + 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x28, 0x01, 0x32, 0xd9, 0x01, 0x0a, 0x12, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x62, 0x0a, 0x13, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, + 0x12, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5f, 0x0a, + 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x73, 0x12, 0x23, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, + 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, + 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x54, 0x0a, 0x06, 0x51, 0x75, 0x6f, 0x74, 0x61, + 0x73, 0x12, 0x4a, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x55, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x1b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x51, 0x75, + 0x6f, 0x74, 0x61, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x51, 0x75, 0x6f, 0x74, 0x61, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3b, 0x5a, + 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, + 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, + 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, }) var ( @@ -3397,7 +3522,7 @@ func file_resource_proto_rawDescGZIP() []byte { } var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 7) -var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 36) +var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 38) var file_resource_proto_goTypes = []any{ (ResourceVersionMatch)(0), // 0: resource.ResourceVersionMatch (ResourceVersionMatchV2)(0), // 1: resource.ResourceVersionMatchV2 @@ -3436,12 +3561,14 @@ var file_resource_proto_goTypes = []any{ (*ResourceTable)(nil), // 34: resource.ResourceTable (*ResourceTableColumnDefinition)(nil), // 35: resource.ResourceTableColumnDefinition (*ResourceTableRow)(nil), // 36: resource.ResourceTableRow - (*WatchEvent_Resource)(nil), // 37: resource.WatchEvent.Resource - (*BulkResponse_Summary)(nil), // 38: resource.BulkResponse.Summary - (*BulkResponse_Rejected)(nil), // 39: resource.BulkResponse.Rejected - (*ListManagedObjectsResponse_Item)(nil), // 40: resource.ListManagedObjectsResponse.Item - (*CountManagedObjectsResponse_ResourceCount)(nil), // 41: resource.CountManagedObjectsResponse.ResourceCount - (*ResourceTableColumnDefinition_Properties)(nil), // 42: resource.ResourceTableColumnDefinition.Properties + (*QuotaUsageRequest)(nil), // 37: resource.QuotaUsageRequest + (*QuotaUsageResponse)(nil), // 38: resource.QuotaUsageResponse + (*WatchEvent_Resource)(nil), // 39: resource.WatchEvent.Resource + (*BulkResponse_Summary)(nil), // 40: resource.BulkResponse.Summary + (*BulkResponse_Rejected)(nil), // 41: resource.BulkResponse.Rejected + (*ListManagedObjectsResponse_Item)(nil), // 42: resource.ListManagedObjectsResponse.Item + (*CountManagedObjectsResponse_ResourceCount)(nil), // 43: resource.CountManagedObjectsResponse.ResourceCount + (*ResourceTableColumnDefinition_Properties)(nil), // 44: resource.ResourceTableColumnDefinition.Properties } var file_resource_proto_depIdxs = []int32{ 10, // 0: resource.ErrorResult.details:type_name -> resource.ErrorDetails @@ -3465,51 +3592,55 @@ var file_resource_proto_depIdxs = []int32{ 9, // 18: resource.ListResponse.error:type_name -> resource.ErrorResult 21, // 19: resource.WatchRequest.options:type_name -> resource.ListOptions 3, // 20: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type - 37, // 21: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource - 37, // 22: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource + 39, // 21: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource + 39, // 22: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource 7, // 23: resource.BulkRequest.key:type_name -> resource.ResourceKey 4, // 24: resource.BulkRequest.action:type_name -> resource.BulkRequest.Action 9, // 25: resource.BulkResponse.error:type_name -> resource.ErrorResult - 38, // 26: resource.BulkResponse.summary:type_name -> resource.BulkResponse.Summary - 39, // 27: resource.BulkResponse.rejected:type_name -> resource.BulkResponse.Rejected - 40, // 28: resource.ListManagedObjectsResponse.items:type_name -> resource.ListManagedObjectsResponse.Item + 40, // 26: resource.BulkResponse.summary:type_name -> resource.BulkResponse.Summary + 41, // 27: resource.BulkResponse.rejected:type_name -> resource.BulkResponse.Rejected + 42, // 28: resource.ListManagedObjectsResponse.items:type_name -> resource.ListManagedObjectsResponse.Item 9, // 29: resource.ListManagedObjectsResponse.error:type_name -> resource.ErrorResult - 41, // 30: resource.CountManagedObjectsResponse.items:type_name -> resource.CountManagedObjectsResponse.ResourceCount + 43, // 30: resource.CountManagedObjectsResponse.items:type_name -> resource.CountManagedObjectsResponse.ResourceCount 9, // 31: resource.CountManagedObjectsResponse.error:type_name -> resource.ErrorResult 5, // 32: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus 35, // 33: resource.ResourceTable.columns:type_name -> resource.ResourceTableColumnDefinition 36, // 34: resource.ResourceTable.rows:type_name -> resource.ResourceTableRow 6, // 35: resource.ResourceTableColumnDefinition.type:type_name -> resource.ResourceTableColumnDefinition.ColumnType - 42, // 36: resource.ResourceTableColumnDefinition.properties:type_name -> resource.ResourceTableColumnDefinition.Properties + 44, // 36: resource.ResourceTableColumnDefinition.properties:type_name -> resource.ResourceTableColumnDefinition.Properties 7, // 37: resource.ResourceTableRow.key:type_name -> resource.ResourceKey - 7, // 38: resource.BulkResponse.Rejected.key:type_name -> resource.ResourceKey - 4, // 39: resource.BulkResponse.Rejected.action:type_name -> resource.BulkRequest.Action - 7, // 40: resource.ListManagedObjectsResponse.Item.object:type_name -> resource.ResourceKey - 18, // 41: resource.ResourceStore.Read:input_type -> resource.ReadRequest - 12, // 42: resource.ResourceStore.Create:input_type -> resource.CreateRequest - 14, // 43: resource.ResourceStore.Update:input_type -> resource.UpdateRequest - 16, // 44: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest - 22, // 45: resource.ResourceStore.List:input_type -> resource.ListRequest - 24, // 46: resource.ResourceStore.Watch:input_type -> resource.WatchRequest - 26, // 47: resource.BulkStore.BulkProcess:input_type -> resource.BulkRequest - 30, // 48: resource.ManagedObjectIndex.CountManagedObjects:input_type -> resource.CountManagedObjectsRequest - 28, // 49: resource.ManagedObjectIndex.ListManagedObjects:input_type -> resource.ListManagedObjectsRequest - 32, // 50: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest - 19, // 51: resource.ResourceStore.Read:output_type -> resource.ReadResponse - 13, // 52: resource.ResourceStore.Create:output_type -> resource.CreateResponse - 15, // 53: resource.ResourceStore.Update:output_type -> resource.UpdateResponse - 17, // 54: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse - 23, // 55: resource.ResourceStore.List:output_type -> resource.ListResponse - 25, // 56: resource.ResourceStore.Watch:output_type -> resource.WatchEvent - 27, // 57: resource.BulkStore.BulkProcess:output_type -> resource.BulkResponse - 31, // 58: resource.ManagedObjectIndex.CountManagedObjects:output_type -> resource.CountManagedObjectsResponse - 29, // 59: resource.ManagedObjectIndex.ListManagedObjects:output_type -> resource.ListManagedObjectsResponse - 33, // 60: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse - 51, // [51:61] is the sub-list for method output_type - 41, // [41:51] is the sub-list for method input_type - 41, // [41:41] is the sub-list for extension type_name - 41, // [41:41] is the sub-list for extension extendee - 0, // [0:41] is the sub-list for field type_name + 7, // 38: resource.QuotaUsageRequest.key:type_name -> resource.ResourceKey + 9, // 39: resource.QuotaUsageResponse.error:type_name -> resource.ErrorResult + 7, // 40: resource.BulkResponse.Rejected.key:type_name -> resource.ResourceKey + 4, // 41: resource.BulkResponse.Rejected.action:type_name -> resource.BulkRequest.Action + 7, // 42: resource.ListManagedObjectsResponse.Item.object:type_name -> resource.ResourceKey + 18, // 43: resource.ResourceStore.Read:input_type -> resource.ReadRequest + 12, // 44: resource.ResourceStore.Create:input_type -> resource.CreateRequest + 14, // 45: resource.ResourceStore.Update:input_type -> resource.UpdateRequest + 16, // 46: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest + 22, // 47: resource.ResourceStore.List:input_type -> resource.ListRequest + 24, // 48: resource.ResourceStore.Watch:input_type -> resource.WatchRequest + 26, // 49: resource.BulkStore.BulkProcess:input_type -> resource.BulkRequest + 30, // 50: resource.ManagedObjectIndex.CountManagedObjects:input_type -> resource.CountManagedObjectsRequest + 28, // 51: resource.ManagedObjectIndex.ListManagedObjects:input_type -> resource.ListManagedObjectsRequest + 32, // 52: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest + 37, // 53: resource.Quotas.GetQuotaUsage:input_type -> resource.QuotaUsageRequest + 19, // 54: resource.ResourceStore.Read:output_type -> resource.ReadResponse + 13, // 55: resource.ResourceStore.Create:output_type -> resource.CreateResponse + 15, // 56: resource.ResourceStore.Update:output_type -> resource.UpdateResponse + 17, // 57: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse + 23, // 58: resource.ResourceStore.List:output_type -> resource.ListResponse + 25, // 59: resource.ResourceStore.Watch:output_type -> resource.WatchEvent + 27, // 60: resource.BulkStore.BulkProcess:output_type -> resource.BulkResponse + 31, // 61: resource.ManagedObjectIndex.CountManagedObjects:output_type -> resource.CountManagedObjectsResponse + 29, // 62: resource.ManagedObjectIndex.ListManagedObjects:output_type -> resource.ListManagedObjectsResponse + 33, // 63: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse + 38, // 64: resource.Quotas.GetQuotaUsage:output_type -> resource.QuotaUsageResponse + 54, // [54:65] is the sub-list for method output_type + 43, // [43:54] is the sub-list for method input_type + 43, // [43:43] is the sub-list for extension type_name + 43, // [43:43] is the sub-list for extension extendee + 0, // [0:43] is the sub-list for field type_name } func init() { file_resource_proto_init() } @@ -3524,9 +3655,9 @@ func file_resource_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_resource_proto_rawDesc), len(file_resource_proto_rawDesc)), NumEnums: 7, - NumMessages: 36, + NumMessages: 38, NumExtensions: 0, - NumServices: 4, + NumServices: 5, }, GoTypes: file_resource_proto_goTypes, DependencyIndexes: file_resource_proto_depIdxs, diff --git a/pkg/storage/unified/resourcepb/resource_grpc.pb.go b/pkg/storage/unified/resourcepb/resource_grpc.pb.go index c90aa0f26a6..a37ab264d76 100644 --- a/pkg/storage/unified/resourcepb/resource_grpc.pb.go +++ b/pkg/storage/unified/resourcepb/resource_grpc.pb.go @@ -709,3 +709,94 @@ var Diagnostics_ServiceDesc = grpc.ServiceDesc{ Streams: []grpc.StreamDesc{}, Metadata: "resource.proto", } + +const ( + Quotas_GetQuotaUsage_FullMethodName = "/resource.Quotas/GetQuotaUsage" +) + +// QuotasClient is the client API for Quotas service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type QuotasClient interface { + // Get current quota usage and limits + GetQuotaUsage(ctx context.Context, in *QuotaUsageRequest, opts ...grpc.CallOption) (*QuotaUsageResponse, error) +} + +type quotasClient struct { + cc grpc.ClientConnInterface +} + +func NewQuotasClient(cc grpc.ClientConnInterface) QuotasClient { + return "asClient{cc} +} + +func (c *quotasClient) GetQuotaUsage(ctx context.Context, in *QuotaUsageRequest, opts ...grpc.CallOption) (*QuotaUsageResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(QuotaUsageResponse) + err := c.cc.Invoke(ctx, Quotas_GetQuotaUsage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// QuotasServer is the server API for Quotas service. +// All implementations should embed UnimplementedQuotasServer +// for forward compatibility +type QuotasServer interface { + // Get current quota usage and limits + GetQuotaUsage(context.Context, *QuotaUsageRequest) (*QuotaUsageResponse, error) +} + +// UnimplementedQuotasServer should be embedded to have forward compatible implementations. +type UnimplementedQuotasServer struct { +} + +func (UnimplementedQuotasServer) GetQuotaUsage(context.Context, *QuotaUsageRequest) (*QuotaUsageResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetQuotaUsage not implemented") +} + +// UnsafeQuotasServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to QuotasServer will +// result in compilation errors. +type UnsafeQuotasServer interface { + mustEmbedUnimplementedQuotasServer() +} + +func RegisterQuotasServer(s grpc.ServiceRegistrar, srv QuotasServer) { + s.RegisterService(&Quotas_ServiceDesc, srv) +} + +func _Quotas_GetQuotaUsage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QuotaUsageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QuotasServer).GetQuotaUsage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Quotas_GetQuotaUsage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QuotasServer).GetQuotaUsage(ctx, req.(*QuotaUsageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Quotas_ServiceDesc is the grpc.ServiceDesc for Quotas service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Quotas_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "resource.Quotas", + HandlerType: (*QuotasServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetQuotaUsage", + Handler: _Quotas_GetQuotaUsage_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "resource.proto", +} diff --git a/pkg/storage/unified/sql/data/resource_history_update_rv.sql b/pkg/storage/unified/sql/data/resource_history_update_rv.sql index 767338ddb34..266227b5317 100644 --- a/pkg/storage/unified/sql/data/resource_history_update_rv.sql +++ b/pkg/storage/unified/sql/data/resource_history_update_rv.sql @@ -5,6 +5,25 @@ SET {{ .Ident "resource_version" }} = ( WHEN {{ $.Ident "guid" }} = {{ $.Arg $guid }} THEN CAST({{ $.Arg $rv }} AS {{ if eq $.DialectName "postgres" }}BIGINT{{ else }}SIGNED{{ end }}) {{ end }} END +), {{ .Ident "key_path" }} = ( + CASE + {{ range $guid, $snowflakeRv := .GUIDToSnowflakeRV }} + WHEN {{ $.Ident "guid" }} = {{ $.Arg $guid }} THEN CONCAT( + 'unified', {{ $.SlashFunc }}, 'data', {{ $.SlashFunc }}, + {{ $.Ident "group" }}, {{ $.SlashFunc }}, + {{ $.Ident "resource" }}, {{ $.SlashFunc }}, + {{ $.Ident "namespace" }}, {{ $.SlashFunc }}, + {{ $.Ident "name" }}, {{ $.SlashFunc }}, + CAST({{ $.Arg $snowflakeRv }} AS {{ if eq $.DialectName "postgres" }}BIGINT{{ else }}SIGNED{{ end }}), + {{ $.TildeFunc }}, + CASE {{ $.Ident "action" }} + WHEN 1 THEN 'created' + WHEN 2 THEN 'updated' + WHEN 3 THEN 'deleted' + END, {{ $.TildeFunc }}, + COALESCE({{ $.Ident "folder" }}, '')) + {{ end }} + END ) WHERE {{ .Ident "guid" }} IN ( {{$first := true}} diff --git a/pkg/storage/unified/sql/db/migrations/resource_mig.go b/pkg/storage/unified/sql/db/migrations/resource_mig.go index c4db9b1dbb9..fbbfe32d4a6 100644 --- a/pkg/storage/unified/sql/db/migrations/resource_mig.go +++ b/pkg/storage/unified/sql/db/migrations/resource_mig.go @@ -198,5 +198,11 @@ func initResourceTables(mg *migrator.Migrator) string { } mg.AddMigration("create table "+resource_events_table.Name, migrator.NewAddTableMigration(resource_events_table)) + mg.AddMigration("Add IDX_resource_history_key_path index", migrator.NewAddIndexMigration(resource_history_table, &migrator.Index{ + Cols: []string{"key_path"}, + Type: migrator.IndexType, + Name: "IDX_resource_history_key_path", + })) + return marker } diff --git a/pkg/storage/unified/sql/queries.go b/pkg/storage/unified/sql/queries.go index e51cf943041..5b9a177e17c 100644 --- a/pkg/storage/unified/sql/queries.go +++ b/pkg/storage/unified/sql/queries.go @@ -369,13 +369,30 @@ func (r sqlResourceBlobQueryRequest) Validate() error { type sqlResourceUpdateRVRequest struct { sqltemplate.SQLTemplate - GUIDToRV map[string]int64 + GUIDToRV map[string]int64 + GUIDToSnowflakeRV map[string]int64 } func (r sqlResourceUpdateRVRequest) Validate() error { return nil // TODO } +func (r sqlResourceUpdateRVRequest) SlashFunc() string { + if r.DialectName() == "postgres" { + return "CHR(47)" + } + + return "CHAR(47)" +} + +func (r sqlResourceUpdateRVRequest) TildeFunc() string { + if r.DialectName() == "postgres" { + return "CHR(126)" + } + + return "CHAR(126)" +} + // resource_version table requests. type resourceVersionResponse struct { ResourceVersion int64 diff --git a/pkg/storage/unified/sql/rv_manager.go b/pkg/storage/unified/sql/rv_manager.go index 1232aa9c700..858345b1fc2 100644 --- a/pkg/storage/unified/sql/rv_manager.go +++ b/pkg/storage/unified/sql/rv_manager.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/bwmarrin/snowflake" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "go.opentelemetry.io/otel/attribute" @@ -240,6 +241,7 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource defer cancel() guidToRV := make(map[string]int64, len(batch)) + guidToSnowflakeRV := make(map[string]int64, len(batch)) guids := make([]string, len(batch)) // The GUIDs of the created resources in the same order as the batch rvs := make([]int64, len(batch)) // The RVs of the created resources in the same order as the batch @@ -285,6 +287,7 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource // Allocate the RVs for i, guid := range guids { guidToRV[guid] = rv + guidToSnowflakeRV[guid] = snowflakeFromRv(rv) rvs[i] = rv rv++ } @@ -301,8 +304,9 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource span.AddEvent("resource_versions_updated") if _, err := dbutil.Exec(ctx, tx, sqlResourceHistoryUpdateRV, sqlResourceUpdateRVRequest{ - SQLTemplate: sqltemplate.New(m.dialect), - GUIDToRV: guidToRV, + SQLTemplate: sqltemplate.New(m.dialect), + GUIDToRV: guidToRV, + GUIDToSnowflakeRV: guidToSnowflakeRV, }); err != nil { span.AddEvent("resource_history_update_rv_failed", trace.WithAttributes( attribute.String("error", err.Error()), @@ -340,6 +344,12 @@ func (m *resourceVersionManager) execBatch(ctx context.Context, group, resource } } +// takes a unix microsecond rv and transforms into a snowflake format. The timestamp is converted from microsecond to +// millisecond (the integer division) and the remainder is saved in the stepbits section. machine id is always 0 +func snowflakeFromRv(rv int64) int64 { + return (((rv / 1000) - snowflake.Epoch) << (snowflake.NodeBits + snowflake.StepBits)) + (rv % 1000) +} + // lock locks the resource version for the given key func (m *resourceVersionManager) lock(ctx context.Context, x db.ContextExecer, group, resource string) (nextRV int64, err error) { // 1. Lock the row and prevent concurrent updates until the transaction is committed diff --git a/pkg/storage/unified/sql/service.go b/pkg/storage/unified/sql/service.go index 22ef4f5daf5..75b3e80fcb0 100644 --- a/pkg/storage/unified/sql/service.go +++ b/pkg/storage/unified/sql/service.go @@ -312,6 +312,7 @@ func (s *service) starting(ctx context.Context) error { resourcepb.RegisterManagedObjectIndexServer(srv, server) resourcepb.RegisterBlobStoreServer(srv, server) resourcepb.RegisterDiagnosticsServer(srv, server) + resourcepb.RegisterQuotasServer(srv, server) grpc_health_v1.RegisterHealthServer(srv, healthService) // register reflection service diff --git a/pkg/storage/unified/sql/test/benchmark_test.go b/pkg/storage/unified/sql/test/benchmark_test.go index f9665c83b7d..8bf65cbd6f7 100644 --- a/pkg/storage/unified/sql/test/benchmark_test.go +++ b/pkg/storage/unified/sql/test/benchmark_test.go @@ -15,5 +15,6 @@ func TestIntegrationBenchmarkSQLStorageBackend(t *testing.T) { if db.IsTestDbSQLite() { opts.Concurrency = 1 // to avoid SQLite database is locked error } - test.BenchmarkStorageBackend(t, newTestBackend(t, true, 2*time.Millisecond), opts) + backend, _ := newTestBackend(t, true, 2*time.Millisecond) + test.BenchmarkStorageBackend(t, backend, opts) } diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index cf45ee64d43..eaf78de0779 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/storage/unified/sql" + sqldb "github.com/grafana/grafana/pkg/storage/unified/sql/db" "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" unitest "github.com/grafana/grafana/pkg/storage/unified/testing" "github.com/grafana/grafana/pkg/tests/testsuite" @@ -38,7 +39,7 @@ var initMutex = &sync.Mutex{} // newTestBackend creates a fresh database and backend for a test. // It uses a mutex to ensure the entire initialization and migration // process is atomic and does not race with other parallel tests. -func newTestBackend(t *testing.T, isHA bool, simulatedNetworkLatency time.Duration) resource.StorageBackend { +func newTestBackend(t *testing.T, isHA bool, simulatedNetworkLatency time.Duration) (resource.StorageBackend, sqldb.DB) { // Lock to ensure the entire init block is atomic. initMutex.Lock() // Unlock once the function returns the initialized backend. @@ -61,7 +62,11 @@ func newTestBackend(t *testing.T, isHA bool, simulatedNetworkLatency time.Durati // Use a context with a reasonable timeout for migrations. err = backend.Init(testutil.NewTestContext(t, time.Now().Add(1*time.Minute))) require.NoError(t, err) - return backend + + sqlDB, err := eDB.Init(testutil.NewTestContext(t, time.Now().Add(1*time.Minute))) + require.NoError(t, err) + + return backend, sqlDB } func TestMain(m *testing.M) { @@ -73,7 +78,8 @@ func TestIntegrationStorageServer(t *testing.T) { t.Cleanup(db.CleanupTestDB) unitest.RunStorageServerTest(t, func(ctx context.Context) resource.StorageBackend { - return newTestBackend(t, true, 0) + backend, _ := newTestBackend(t, true, 0) + return backend }) } @@ -84,12 +90,31 @@ func TestIntegrationSQLStorageBackend(t *testing.T) { t.Run("IsHA (polling notifier)", func(t *testing.T) { unitest.RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { - return newTestBackend(t, true, 0) + backend, _ := newTestBackend(t, true, 0) + return backend }, nil) }) t.Run("NotHA (in process notifier)", func(t *testing.T) { unitest.RunStorageBackendTest(t, func(ctx context.Context) resource.StorageBackend { + backend, _ := newTestBackend(t, false, 0) + return backend + }, nil) + }) +} + +func TestIntegrationSQLStorageAndSQLKVCompatibilityTests(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + t.Cleanup(db.CleanupTestDB) + + t.Run("IsHA (polling notifier)", func(t *testing.T) { + unitest.RunSQLStorageBackendCompatibilityTest(t, func(ctx context.Context) (resource.StorageBackend, sqldb.DB) { + return newTestBackend(t, true, 0) + }, nil) + }) + + t.Run("NotHA (in process notifier)", func(t *testing.T) { + unitest.RunSQLStorageBackendCompatibilityTest(t, func(ctx context.Context) (resource.StorageBackend, sqldb.DB) { return newTestBackend(t, false, 0) }, nil) }) @@ -110,7 +135,7 @@ func TestIntegrationSearchAndStorage(t *testing.T) { t.Cleanup(search.Stop) // Create a new resource backend - storage := newTestBackend(t, false, 0) + storage, _ := newTestBackend(t, false, 0) require.NotNil(t, storage) // Run the shared storage and search tests diff --git a/pkg/storage/unified/sql/testdata/mysql--resource_history_update_rv-single path.sql b/pkg/storage/unified/sql/testdata/mysql--resource_history_update_rv-single path.sql index 6b07ff5e55b..62bb64c8300 100755 --- a/pkg/storage/unified/sql/testdata/mysql--resource_history_update_rv-single path.sql +++ b/pkg/storage/unified/sql/testdata/mysql--resource_history_update_rv-single path.sql @@ -4,6 +4,9 @@ SET `resource_version` = ( WHEN `guid` = 'guid1' THEN CAST(123 AS SIGNED) WHEN `guid` = 'guid2' THEN CAST(456 AS SIGNED) END +), `key_path` = ( + CASE + END ) WHERE `guid` IN ( 'guid1', 'guid2' diff --git a/pkg/storage/unified/sql/testdata/postgres--resource_history_update_rv-single path.sql b/pkg/storage/unified/sql/testdata/postgres--resource_history_update_rv-single path.sql index 85352e9ead4..6529fb1e4d5 100755 --- a/pkg/storage/unified/sql/testdata/postgres--resource_history_update_rv-single path.sql +++ b/pkg/storage/unified/sql/testdata/postgres--resource_history_update_rv-single path.sql @@ -4,6 +4,9 @@ SET "resource_version" = ( WHEN "guid" = 'guid1' THEN CAST(123 AS BIGINT) WHEN "guid" = 'guid2' THEN CAST(456 AS BIGINT) END +), "key_path" = ( + CASE + END ) WHERE "guid" IN ( 'guid1', 'guid2' diff --git a/pkg/storage/unified/sql/testdata/sqlite--resource_history_update_rv-single path.sql b/pkg/storage/unified/sql/testdata/sqlite--resource_history_update_rv-single path.sql index 9fb23fb3956..707fbad6cb8 100755 --- a/pkg/storage/unified/sql/testdata/sqlite--resource_history_update_rv-single path.sql +++ b/pkg/storage/unified/sql/testdata/sqlite--resource_history_update_rv-single path.sql @@ -4,6 +4,9 @@ SET "resource_version" = ( WHEN "guid" = 'guid1' THEN CAST(123 AS SIGNED) WHEN "guid" = 'guid2' THEN CAST(456 AS SIGNED) END +), "key_path" = ( + CASE + END ) WHERE "guid" IN ( 'guid1', 'guid2' diff --git a/pkg/storage/unified/testing/storage_backend.go b/pkg/storage/unified/testing/storage_backend.go index ce36d8836bc..730efe418f6 100644 --- a/pkg/storage/unified/testing/storage_backend.go +++ b/pkg/storage/unified/testing/storage_backend.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/bwmarrin/snowflake" "github.com/go-jose/go-jose/v4/jwt" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -25,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + sqldb "github.com/grafana/grafana/pkg/storage/unified/sql/db" "github.com/grafana/grafana/pkg/util/testutil" ) @@ -42,10 +44,14 @@ const ( TestCreateNewResource = "create new resource" TestGetResourceLastImportTime = "get resource last import time" TestOptimisticLocking = "optimistic locking on concurrent writes" + TestKeyPathGeneration = "key_path generation" ) type NewBackendFunc func(ctx context.Context) resource.StorageBackend +// NewBackendWithDBFunc creates a backend with database access for testing +type NewBackendWithDBFunc func(ctx context.Context) (resource.StorageBackend, sqldb.DB) + // TestOptions configures which tests to run type TestOptions struct { SkipTests map[string]bool // tests to skip @@ -100,6 +106,37 @@ func RunStorageBackendTest(t *testing.T, newBackend NewBackendFunc, opts *TestOp } } +func RunSQLStorageBackendCompatibilityTest(t *testing.T, newBackend NewBackendWithDBFunc, opts *TestOptions) { + if opts == nil { + opts = &TestOptions{} + } + + if opts.NSPrefix == "" { + opts.NSPrefix = GenerateRandomNSPrefix() + } + + t.Logf("Running tests with namespace prefix: %s", opts.NSPrefix) + + cases := []struct { + name string + fn func(*testing.T, resource.StorageBackend, string, sqldb.DB) + }{ + {TestKeyPathGeneration, runTestIntegrationBackendKeyPathGeneration}, + } + + for _, tc := range cases { + if shouldSkip := opts.SkipTests[tc.name]; shouldSkip { + t.Logf("Skipping test: %s", tc.name) + continue + } + + t.Run(tc.name, func(t *testing.T) { + backend, db := newBackend(context.Background()) + tc.fn(t, backend, opts.NSPrefix, db) + }) + } +} + func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBackend, nsPrefix string) { ctx := types.WithAuthInfo(context.Background(), authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{ Claims: jwt.Claims{ @@ -1722,3 +1759,222 @@ func runTestIntegrationBackendOptimisticLocking(t *testing.T, backend resource.S require.LessOrEqual(t, successes, 1, "at most one create should succeed (errors: %v)", errorMessages) }) } + +func runTestIntegrationBackendKeyPathGeneration(t *testing.T, backend resource.StorageBackend, nsPrefix string, db sqldb.DB) { + ctx := testutil.NewDefaultTestContext(t) + + t.Run("Create resource", func(t *testing.T) { + // Create a test resource + key := &resourcepb.ResourceKey{ + Group: "playlist.grafana.app", + Resource: "playlists", + Namespace: nsPrefix + "-default", + Name: "test-playlist-crud", + } + + // Create the K8s unstructured object + testObj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "playlist.grafana.app/v0alpha1", + "kind": "Playlist", + "metadata": map[string]interface{}{ + "name": "test-playlist-crud", + "namespace": nsPrefix + "-default", + "uid": "test-uid-crud-123", + }, + "spec": map[string]interface{}{ + "title": "My Test Playlist", + }, + }, + } + + // Get metadata accessor + metaAccessor, err := utils.MetaAccessor(testObj) + require.NoError(t, err) + + // Serialize to JSON + jsonBytes, err := testObj.MarshalJSON() + require.NoError(t, err) + + // Create WriteEvent + writeEvent := resource.WriteEvent{ + Type: resourcepb.WatchEvent_ADDED, + Key: key, + Value: jsonBytes, + Object: metaAccessor, + PreviousRV: 0, // Always 0 for new resources + GUID: "create-guid-crud-123", + } + + // Create the resource using WriteEvent + createRV, err := backend.WriteEvent(ctx, writeEvent) + require.NoError(t, err) + require.Greater(t, createRV, int64(0)) + + // Verify created resource key_path + verifyKeyPath(t, db, ctx, key, "created", createRV, "") + + t.Run("Update resource", func(t *testing.T) { + // Update the resource + testObj.Object["spec"] = map[string]interface{}{ + "title": "My Updated Playlist", + } + + updatedMetaAccessor, err := utils.MetaAccessor(testObj) + require.NoError(t, err) + + updatedJsonBytes, err := testObj.MarshalJSON() + require.NoError(t, err) + + updateEvent := resource.WriteEvent{ + Type: resourcepb.WatchEvent_MODIFIED, + Key: key, + Value: updatedJsonBytes, + Object: updatedMetaAccessor, + PreviousRV: createRV, + GUID: fmt.Sprintf("update-guid-%d", createRV), + } + + // Update the resource + updateRV, err := backend.WriteEvent(ctx, updateEvent) + require.NoError(t, err) + require.Greater(t, updateRV, createRV) + + // Verify updated resource key_path + verifyKeyPath(t, db, ctx, key, "updated", updateRV, "") + + t.Run("Delete resource", func(t *testing.T) { + deleteEvent := resource.WriteEvent{ + Type: resourcepb.WatchEvent_DELETED, + Key: key, + Value: updatedJsonBytes, // Keep the last known value + Object: updatedMetaAccessor, + PreviousRV: updateRV, + GUID: fmt.Sprintf("delete-guid-%d", updateRV), + } + + // Delete the resource + deleteRV, err := backend.WriteEvent(ctx, deleteEvent) + require.NoError(t, err) + require.Greater(t, deleteRV, updateRV) + + // Verify deleted resource key_path + verifyKeyPath(t, db, ctx, key, "deleted", deleteRV, "") + }) + }) + }) + + t.Run("Resource with folder", func(t *testing.T) { + // Create a resource in a folder + folderKey := &resourcepb.ResourceKey{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Namespace: nsPrefix + "-default", + Name: "my-dashboard", + } + + // Create dashboard object with folder + dashboardObj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "dashboard.grafana.app/v0alpha1", + "kind": "Dashboard", + "metadata": map[string]interface{}{ + "name": "my-dashboard", + "namespace": nsPrefix + "-default", + "uid": "dash-uid-456", + "annotations": map[string]interface{}{ + "grafana.app/folder": "test-folder", + }, + }, + "spec": map[string]interface{}{ + "title": "My Dashboard", + }, + }, + } + + folderMetaAccessor, err := utils.MetaAccessor(dashboardObj) + require.NoError(t, err) + + folderJsonBytes, err := dashboardObj.MarshalJSON() + require.NoError(t, err) + + folderWriteEvent := resource.WriteEvent{ + Type: resourcepb.WatchEvent_ADDED, + Key: folderKey, + Value: folderJsonBytes, + Object: folderMetaAccessor, + PreviousRV: 0, + GUID: "folder-guid-456", + } + + // Create the dashboard in folder + folderRV, err := backend.WriteEvent(ctx, folderWriteEvent) + require.NoError(t, err) + require.Greater(t, folderRV, int64(0)) + + // Verify folder resource key_path includes folder + verifyKeyPath(t, db, ctx, folderKey, "created", folderRV, "test-folder") + }) +} + +// verifyKeyPath is a helper function to verify key_path generation +func verifyKeyPath(t *testing.T, db sqldb.DB, ctx context.Context, key *resourcepb.ResourceKey, action string, resourceVersion int64, expectedFolder string) { + var query string + if db.DriverName() == "postgres" { + query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE namespace = $1 AND name = $2 AND resource_version = $3" + } else { + query = "SELECT key_path, resource_version, action, folder FROM resource_history WHERE namespace = ? AND name = ? AND resource_version = ?" + } + rows, err := db.QueryContext(ctx, query, key.Namespace, key.Name, resourceVersion) + require.NoError(t, err) + + require.True(t, rows.Next()) + + var keyPath string + var actualRV int64 + var actualAction int + var actualFolder string + + err = rows.Scan(&keyPath, &actualRV, &actualAction, &actualFolder) + require.NoError(t, err) + err = rows.Close() + require.NoError(t, err) + + // Verify basic key_path format + require.Contains(t, keyPath, "unified/data/") + require.Contains(t, keyPath, key.Group) + require.Contains(t, keyPath, key.Resource) + require.Contains(t, keyPath, key.Namespace) + require.Contains(t, keyPath, key.Name) + + // Verify action suffix + require.Contains(t, keyPath, fmt.Sprintf("~%s~", action)) + + // Verify snowflake calculation + expectedSnowflake := (((resourceVersion / 1000) - snowflake.Epoch) << (snowflake.NodeBits + snowflake.StepBits)) + (resourceVersion % 1000) + require.Contains(t, keyPath, fmt.Sprintf("/%d~", expectedSnowflake), fmt.Sprintf("actual RV: %d", actualRV)) + + // Verify folder if specified + if expectedFolder != "" { + require.Equal(t, expectedFolder, actualFolder) + require.Contains(t, keyPath, expectedFolder) + } + + // Verify action code matches + var expectedActionCode int + switch action { + case "created": + expectedActionCode = 1 + case "updated": + expectedActionCode = 2 + case "deleted": + expectedActionCode = 3 + } + require.Equal(t, expectedActionCode, actualAction) + + t.Logf("Action: %s, RV: %d, Snowflake: %d", action, resourceVersion, expectedSnowflake) + t.Logf("Key_path: %s", keyPath) + if expectedFolder != "" { + t.Logf("Folder: %s", actualFolder) + } +} diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index 50ab7d939ea..fcfb09cb5ba 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -233,6 +233,7 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) { err = resp.Body.Close() require.NoError(t, err) }) + t.Run("When updating uid with a dashboard already using that uid", func(t *testing.T) { resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ "dashboard": map[string]interface{}{ @@ -266,6 +267,34 @@ func TestIntegrationDashboardServiceValidation(t *testing.T) { err = resp.Body.Close() require.NoError(t, err) }) + + t.Run("When creating a dashboard that references a non-existent library panel", func(t *testing.T) { + originalCount := getDashboardCount(t, grafanaListedAddr, "admin", "admin") + resp, err := postDashboard(t, grafanaListedAddr, "admin", "admin", map[string]interface{}{ + "dashboard": map[string]interface{}{ + "title": "Bad dashboard", + "panels": []interface{}{ + map[string]interface{}{ + "gridPos": map[string]int{"h": 0, "w": 0, "x": 0, "y": 0}, + "libraryPanel": map[string]string{ + "name": "Bad panel", + "uid": "invalid-uid", + }, + }, + }, + }, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "library element could not be found") + err = resp.Body.Close() + require.NoError(t, err) + + // A new dashboard is not created in this situation. + require.Equal(t, originalCount, getDashboardCount(t, grafanaListedAddr, "admin", "admin")) + }) } func TestIntegrationDashboardQuota(t *testing.T) { @@ -982,6 +1011,21 @@ func postDashboard(t *testing.T, grafanaListedAddr, user, password string, paylo return http.Post(u, "application/json", bytes.NewBuffer(payloadBytes)) // nolint:gosec } +func getDashboardCount(t *testing.T, grafanaListenAddr, user, password string) int { + endpoint := fmt.Sprintf("http://%s:%s@%s/apis/dashboard.grafana.app/v0alpha1/namespaces/default/search", user, password, grafanaListenAddr) + resp, err := http.Get(endpoint) //nolint:gosec + require.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + var payload map[string]any + require.NoError(t, json.Unmarshal(body, &payload)) + + return int(payload["totalHits"].(float64)) +} + func TestIntegrationDashboardServicePermissions(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) diff --git a/pkg/tests/apis/config_test.go b/pkg/tests/apis/config_test.go index bf00affdfff..a9e8d87e0a0 100644 --- a/pkg/tests/apis/config_test.go +++ b/pkg/tests/apis/config_test.go @@ -15,19 +15,19 @@ const pluginsDiscoveryJSON = `[ "freshness": "Current", "resources": [ { - "resource": "pluginmetas", + "resource": "metas", "responseKind": { "group": "", - "kind": "PluginMeta", + "kind": "Meta", "version": "" }, "scope": "Namespaced", - "singularResource": "pluginmeta", + "singularResource": "meta", "subresources": [ { "responseKind": { "group": "", - "kind": "PluginMeta", + "kind": "Meta", "version": "" }, "subresource": "status", diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index 85f369c2ac6..5d5d04aa535 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -293,6 +293,199 @@ func TestIntegrationLegacySupport(t *testing.T) { require.Equal(t, dashboardV0.VERSION, rsp.Result.Meta.APIVersion) } +func TestIntegrationListPagination(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + gvr := schema.GroupVersionResource{ + Group: dashboardV0.GROUP, + Version: dashboardV0.VERSION, + Resource: "dashboards", + } + + // Test on modes with legacy + modes := []rest.DualWriterMode{rest.Mode1, rest.Mode2, rest.Mode3} + for _, mode := range modes { + t.Run(fmt.Sprintf("pagination with dual writer mode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + DisableDataMigrations: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: mode, + }, + }, + }) + t.Cleanup(helper.Shutdown) + + ctx := context.Background() + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvr, + }) + + // Test 1: List with no dashboards + rsp, err := client.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, rsp.Items, 0) + + // Create 5 dashboards to test pagination with small limits + const totalDashboards = 5 + createdNames := make([]string, 0, totalDashboards) + for i := 0; i < totalDashboards; i++ { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]any{ + "title": fmt.Sprintf("Pagination test dashboard %d", i), + "schemaVersion": 42, + }, + }, + } + obj.SetGenerateName("pag-") + obj.SetAPIVersion(gvr.GroupVersion().String()) + obj.SetKind("Dashboard") + created, err := client.Resource.Create(ctx, obj, metav1.CreateOptions{}) + require.NoError(t, err) + createdNames = append(createdNames, created.GetName()) + } + + // Test 2: List all without limit - should return all dashboards + rsp, err = client.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, rsp.Items, totalDashboards, "should return all %d dashboards", totalDashboards) + + // Test 3: List with small limit (2) - should paginate + const pageSize = 2 + allNames := make(map[string]bool) + continueToken := "" + pageCount := 0 + + for { + pageCount++ + listOpts := metav1.ListOptions{ + Limit: pageSize, + Continue: continueToken, + } + rsp, err = client.Resource.List(ctx, listOpts) + require.NoError(t, err) + + // Collect names from this page + for _, item := range rsp.Items { + name := item.GetName() + require.False(t, allNames[name], "duplicate item %s found across pages", name) + allNames[name] = true + } + + // Check if there's more pages + continueToken = rsp.GetContinue() + if continueToken == "" { + break + } + + // Safety check to prevent infinite loops + require.Less(t, pageCount, 5) + } + + // Verify we got all dashboards across all pages + require.Len(t, allNames, totalDashboards, "should have collected all %d dashboards across pages", totalDashboards) + + // Verify all created dashboards were found + for _, name := range createdNames { + require.True(t, allNames[name], "dashboard %s not found in paginated results", name) + } + }) + + t.Run(fmt.Sprintf("history pagination with dual writer mode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + DisableDataMigrations: true, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: mode, + }, + }, + }) + t.Cleanup(helper.Shutdown) + + ctx := context.Background() + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvr, + }) + + // Create a dashboard + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]any{ + "title": "History pagination test dashboard", + "schemaVersion": 42, + }, + }, + } + obj.SetGenerateName("hist-") + obj.SetAPIVersion(gvr.GroupVersion().String()) + obj.SetKind("Dashboard") + created, err := client.Resource.Create(ctx, obj, metav1.CreateOptions{}) + require.NoError(t, err) + dashName := created.GetName() + + // Update the dashboard multiple times to create history entries + const totalVersions = 5 + for i := 1; i < totalVersions; i++ { + // Get latest version + current, err := client.Resource.Get(ctx, dashName, metav1.GetOptions{}) + require.NoError(t, err) + + // Update title + spec := current.Object["spec"].(map[string]interface{}) + spec["title"] = fmt.Sprintf("History pagination test dashboard v%d", i+1) + current.Object["spec"] = spec + + _, err = client.Resource.Update(ctx, current, metav1.UpdateOptions{}) + require.NoError(t, err) + } + + // Test: List history with pagination + labelSelector := utils.LabelKeyGetHistory + "=true" + fieldSelector := "metadata.name=" + dashName + + const pageSize int64 = 2 + allVersions := make([]string, 0) + continueToken := "" + pageCount := 0 + + for { + pageCount++ + listOpts := metav1.ListOptions{ + LabelSelector: labelSelector, + FieldSelector: fieldSelector, + Limit: pageSize, + Continue: continueToken, + } + rsp, err := client.Resource.List(ctx, listOpts) + require.NoError(t, err) + + // Collect resource versions from this page + for _, item := range rsp.Items { + rv := item.GetResourceVersion() + allVersions = append(allVersions, rv) + } + + // Check if there's more pages + continueToken = rsp.GetContinue() + if continueToken == "" { + break + } + + // Safety check to prevent infinite loops + require.Less(t, pageCount, 5) + } + + // Verify we got all history versions + require.Len(t, allVersions, totalVersions, "should have collected all %d history versions across pages", totalVersions) + }) + } +} + func TestIntegrationSearchTypeFiltering(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index c16eb818a2a..ade90ae209f 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -1879,6 +1879,14 @@ func TestIntegrationMoveNestedFolderToRootK8S(t *testing.T) { require.Equal(t, http.StatusOK, get.Response.StatusCode) require.Equal(t, "f2", get.Result.UID) require.Equal(t, "", get.Result.ParentUID) + + // Check that we can get the same folder using metadata.name selector + results, err := client.Resource.List(context.Background(), metav1.ListOptions{ + FieldSelector: "metadata.name=" + get.Result.UID, + }) + require.NoError(t, err) + require.Len(t, results.Items, 1) + require.Equal(t, "f2", results.Items[0].GetName()) } // Test deleting nested folders ensures postorder deletion diff --git a/pkg/tests/apis/iam/team_search_integration_test.go b/pkg/tests/apis/iam/team_search_integration_test.go new file mode 100644 index 00000000000..c01ca9a641a --- /dev/null +++ b/pkg/tests/apis/iam/team_search_integration_test.go @@ -0,0 +1,202 @@ +package identity + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationTeamSearch(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + // TODO: Add rest.Mode3 and rest.Mode4 when they're supported + modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2} + for _, mode := range modes { + t.Run(fmt.Sprintf("Team search with dual writer mode %d", mode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: false, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "teams.iam.grafana.app": { + DualWriterMode: mode, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, + featuremgmt.FlagKubernetesAuthnMutation, + }, + }) + doTeamSearchTests(t, helper) + }) + } +} + +func doTeamSearchTests(t *testing.T, helper *apis.K8sTestHelper) { + ctx := context.Background() + namespace := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()) + + // Create teams for testing + teamClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: namespace, + GVR: gvrTeams, + }) + + team1, err := teamClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/team-test-create-v0.yaml"), metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, team1) + + // Create a second team with a different name + team2YAML := helper.LoadYAMLOrJSONFile("testdata/team-test-create-v0.yaml") + team2YAML.Object["metadata"].(map[string]interface{})["name"] = "testteam2" + team2YAML.Object["spec"].(map[string]interface{})["title"] = "Another Team" + team2YAML.Object["spec"].(map[string]interface{})["email"] = "anotherteam@example.com" + + team2, err := teamClient.Resource.Create(ctx, team2YAML, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, team2) + + t.Run("should search teams without query parameter", func(t *testing.T) { + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams", namespace) + var result iamv0alpha1.TeamSearchResults + + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path, + }, &result) + + require.NotNil(t, response) + require.Equal(t, http.StatusOK, response.Response.StatusCode) + require.NotNil(t, response.Result) + require.GreaterOrEqual(t, result.TotalHits, int64(2), "should find at least 2 teams") + require.GreaterOrEqual(t, len(result.Hits), 2, "should return at least 2 hits") + + for _, hit := range result.Hits { + if hit.Name == team1.GetName() { + require.Equal(t, "Test Team 1", hit.Title) + require.Equal(t, "testteam1@example123.com", hit.Email) + } + if hit.Name == team2.GetName() { + require.Equal(t, "Another Team", hit.Title) + require.Equal(t, "anotherteam@example.com", hit.Email) + } + } + }) + + t.Run("should search teams with query parameter", func(t *testing.T) { + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?query=another", namespace) + var result iamv0alpha1.TeamSearchResults + + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path, + }, &result) + + require.NotNil(t, response) + require.Equal(t, http.StatusOK, response.Response.StatusCode) + require.NotNil(t, response.Result) + require.Equal(t, result.TotalHits, int64(1), "should find 1 team matching 'another'") + require.Equal(t, len(result.Hits), 1, "should return 1 hit") + require.Equal(t, result.Hits[0].Name, team2.GetName()) + require.Equal(t, result.Hits[0].Title, "Another Team") + require.Equal(t, result.Hits[0].Email, "anotherteam@example.com") + }) + + t.Run("should return no results when query does not match any teams", func(t *testing.T) { + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?query=nonexistent", namespace) + var result iamv0alpha1.TeamSearchResults + + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path, + }, &result) + + require.NotNil(t, response) + require.Equal(t, http.StatusOK, response.Response.StatusCode) + require.NotNil(t, response.Result) + require.Equal(t, int64(0), result.TotalHits, "should return 0 hits when query does not match any teams") + require.Equal(t, 0, len(result.Hits), "should return 0 hits when query does not match any teams") + }) + + t.Run("should search teams with limit parameter", func(t *testing.T) { + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?limit=1", namespace) + var result iamv0alpha1.TeamSearchResults + + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path, + }, &result) + + require.NotNil(t, response) + require.Equal(t, http.StatusOK, response.Response.StatusCode) + require.NotNil(t, response.Result) + require.Equal(t, 1, len(result.Hits), "should return 1 hit when limit is 1") + }) + + t.Run("should search teams with pagination", func(t *testing.T) { + // First page + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?limit=1&page=1", namespace) + var result1 iamv0alpha1.TeamSearchResults + + response1 := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path, + }, &result1) + + require.NotNil(t, response1) + require.Equal(t, http.StatusOK, response1.Response.StatusCode) + require.NotNil(t, response1.Result) + require.Equal(t, int64(0), result1.Offset, "first page should have offset 0") + + // Second page + path2 := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?limit=1&page=2", namespace) + var result2 iamv0alpha1.TeamSearchResults + + response2 := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path2, + }, &result2) + + require.NotNil(t, response2) + require.Equal(t, http.StatusOK, response2.Response.StatusCode) + require.NotNil(t, response2.Result) + require.Equal(t, int64(1), result2.Offset, "second page should have offset 1") + }) + + t.Run("should search teams with offset parameter", func(t *testing.T) { + path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?offset=1&limit=1", namespace) + var result iamv0alpha1.TeamSearchResults + + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodGet, + Path: path, + }, &result) + + require.NotNil(t, response) + require.Equal(t, http.StatusOK, response.Response.StatusCode) + require.NotNil(t, response.Result) + require.GreaterOrEqual(t, result.TotalHits, int64(2), "should find at least 2 teams") + require.Equal(t, 1, len(result.Hits), "should return 1 hit") + require.Equal(t, int64(1), result.Offset, "should return offset 1") + }) +} diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index 2cad6213d04..b0d21a3a60c 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -3020,6 +3020,9 @@ "type": "string", "default": "" }, + "regexApplyTo": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRegexApplyTo" + }, "skipUrlSync": { "type": "boolean", "default": false @@ -3930,6 +3933,14 @@ "onTimeRangeChanged" ] }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRegexApplyTo": { + "description": "Determine whether regex applies to variable value or display text\nAccepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)", + "type": "string", + "enum": [ + "value", + "text" + ] + }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableSort": { "description": "Sort variable options\nAccepted values are:\n`disabled`: No sorting\n`alphabeticalAsc`: Alphabetical ASC\n`alphabeticalDesc`: Alphabetical DESC\n`numericalAsc`: Numerical ASC\n`numericalDesc`: Numerical DESC\n`alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC\n`alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC\n`naturalAsc`: Natural ASC\n`naturalDesc`: Natural DESC\nVariableSort enum with default value", "type": "string", diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json index 198ad3aea25..f4ecc6c4599 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2beta1.json @@ -3047,6 +3047,9 @@ "type": "string", "default": "" }, + "regexApplyTo": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRegexApplyTo" + }, "skipUrlSync": { "type": "boolean", "default": false @@ -3957,6 +3960,14 @@ "onTimeRangeChanged" ] }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRegexApplyTo": { + "description": "Determine whether regex applies to variable value or display text\nAccepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)", + "type": "string", + "enum": [ + "value", + "text" + ] + }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableSort": { "description": "Sort variable options\nAccepted values are:\n`disabled`: No sorting\n`alphabeticalAsc`: Alphabetical ASC\n`alphabeticalDesc`: Alphabetical DESC\n`numericalAsc`: Numerical ASC\n`numericalDesc`: Numerical DESC\n`alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC\n`alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC\n`naturalAsc`: Natural ASC\n`naturalDesc`: Natural DESC\nVariableSort enum with default value", "type": "string", diff --git a/pkg/tests/apis/openapi_snapshots/logsdrilldown.grafana.app-v1alpha1.json b/pkg/tests/apis/openapi_snapshots/logsdrilldown.grafana.app-v1alpha1.json index 25d6cd87807..93be607fd41 100644 --- a/pkg/tests/apis/openapi_snapshots/logsdrilldown.grafana.app-v1alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/logsdrilldown.grafana.app-v1alpha1.json @@ -35,6 +35,1126 @@ } } }, + "/apis/logsdrilldown.grafana.app/v1alpha1/namespaces/{namespace}/logsdrilldowndefaultcolumns": { + "get": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "list or watch objects of kind LogsDrilldownDefaultColumns", + "operationId": "listLogsDrilldownDefaultColumns", + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "post": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "create LogsDrilldownDefaultColumns", + "operationId": "createLogsDrilldownDefaultColumns", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "delete": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "delete collection of LogsDrilldownDefaultColumns", + "operationId": "deletecollectionLogsDrilldownDefaultColumns", + "parameters": [ + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/logsdrilldown.grafana.app/v1alpha1/namespaces/{namespace}/logsdrilldowndefaultcolumns/{name}": { + "get": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "read the specified LogsDrilldownDefaultColumns", + "operationId": "getLogsDrilldownDefaultColumns", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "put": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "replace the specified LogsDrilldownDefaultColumns", + "operationId": "replaceLogsDrilldownDefaultColumns", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "delete": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "delete LogsDrilldownDefaultColumns", + "operationId": "deleteLogsDrilldownDefaultColumns", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "patch": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "partially update the specified LogsDrilldownDefaultColumns", + "operationId": "updateLogsDrilldownDefaultColumns", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the LogsDrilldownDefaultColumns", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/logsdrilldown.grafana.app/v1alpha1/namespaces/{namespace}/logsdrilldowndefaultcolumns/{name}/status": { + "get": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "read status of the specified LogsDrilldownDefaultColumns", + "operationId": "getLogsDrilldownDefaultColumnsStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "put": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "replace status of the specified LogsDrilldownDefaultColumns", + "operationId": "replaceLogsDrilldownDefaultColumnsStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "patch": { + "tags": [ + "LogsDrilldownDefaultColumns" + ], + "description": "partially update status of the specified LogsDrilldownDefaultColumns", + "operationId": "updateLogsDrilldownDefaultColumnsStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "logsdrilldown.grafana.app", + "version": "v1alpha1", + "kind": "LogsDrilldownDefaultColumns" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the LogsDrilldownDefaultColumns", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, "/apis/logsdrilldown.grafana.app/v1alpha1/namespaces/{namespace}/logsdrilldowndefaults": { "get": { "tags": [ @@ -2318,6 +3438,204 @@ } ] }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns": { + "type": "object", + "required": [ + "kind", + "apiVersion", + "metadata", + "spec" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsSpec" + }, + "status": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsStatus" + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "logsdrilldown.grafana.app", + "kind": "LogsDrilldownDefaultColumns", + "version": "v1alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumns" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "logsdrilldown.grafana.app", + "kind": "LogsDrilldownDefaultColumnsList", + "version": "v1alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel": { + "type": "object", + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsLabels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsLabel" + } + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord": { + "type": "object", + "required": [ + "columns", + "labels" + ], + "properties": { + "columns": { + "type": "array", + "items": { + "type": "string" + } + }, + "labels": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsLabels" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsRecords": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsRecord" + } + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsOperatorState": { + "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", + "additionalProperties": {} + } + }, + "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.", + "type": "string", + "enum": [ + "success", + "in_progress", + "failed" + ] + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsSpec": { + "type": "object", + "required": [ + "records" + ], + "properties": { + "records": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsLogsDefaultColumnsRecords" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + }, + "operatorStates": { + "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", + "additionalProperties": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaultColumnsOperatorState" + } + } + }, + "additionalProperties": false + }, "com.github.grafana.grafana.apps.logsdrilldown.pkg.apis.logsdrilldown.v1alpha1.LogsDrilldownDefaults": { "type": "object", "required": [ diff --git a/pkg/tests/apis/plugins/discovery_test.go b/pkg/tests/apis/plugins/discovery_test.go index a6e507a630b..5e211cc45a1 100644 --- a/pkg/tests/apis/plugins/discovery_test.go +++ b/pkg/tests/apis/plugins/discovery_test.go @@ -21,19 +21,19 @@ func TestIntegrationPluginsIntegrationDiscovery(t *testing.T) { "freshness": "Current", "resources": [ { - "resource": "pluginmetas", + "resource": "metas", "responseKind": { "group": "", - "kind": "PluginMeta", + "kind": "Meta", "version": "" }, "scope": "Namespaced", - "singularResource": "pluginmeta", + "singularResource": "meta", "subresources": [ { "responseKind": { "group": "", - "kind": "PluginMeta", + "kind": "Meta", "version": "" }, "subresource": "status", diff --git a/pkg/tests/apis/plugins/pluginmeta_test.go b/pkg/tests/apis/plugins/metas_test.go similarity index 52% rename from pkg/tests/apis/plugins/pluginmeta_test.go rename to pkg/tests/apis/plugins/metas_test.go index af70f02a6f8..c240d3fb91d 100644 --- a/pkg/tests/apis/plugins/pluginmeta_test.go +++ b/pkg/tests/apis/plugins/metas_test.go @@ -15,6 +15,72 @@ import ( func TestIntegrationPluginMeta(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) + t.Run("list plugin metas", func(t *testing.T) { + helper := setupHelper(t) + ctx := context.Background() + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvrPlugins, + }) + + plugin1Name := "test-plugin-metas-1" + plugin1 := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + "apiVersion": "plugins.grafana.app/v0alpha1", + "kind": "Plugin", + "metadata": {"name": "%s"}, + "spec": {"id": "grafana-piechart-panel", "version": "1.0.0"} + }`, plugin1Name)) + _, err := client.Resource.Create(ctx, plugin1, metav1.CreateOptions{}) + require.NoError(t, err) + + plugin2Name := "test-plugin-metas-2" + plugin2 := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ + "apiVersion": "plugins.grafana.app/v0alpha1", + "kind": "Plugin", + "metadata": {"name": "%s"}, + "spec": {"id": "grafana-clock-panel", "version": "1.0.0"} + }`, plugin2Name)) + _, err = client.Resource.Create(ctx, plugin2, metav1.CreateOptions{}) + require.NoError(t, err) + + namespace := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()) + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/metas", namespace) + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, &pluginsv0alpha1.MetaList{}) + + require.NotNil(t, response.Result) + require.NotNil(t, response.Result.Items) + require.GreaterOrEqual(t, len(response.Result.Items), 2) + + foundIDs := make(map[string]bool) + for _, item := range response.Result.Items { + require.NotNil(t, item.Spec.PluginJSON) + foundIDs[item.Spec.PluginJSON.Id] = true + require.NotEmpty(t, item.Spec.PluginJSON.Id) + require.NotEmpty(t, item.Spec.PluginJSON.Type) + require.NotEmpty(t, item.Spec.PluginJSON.Name) + } + require.True(t, foundIDs["grafana-piechart-panel"]) + require.True(t, foundIDs["grafana-clock-panel"]) + }) + + t.Run("list plugin metas with no plugins", func(t *testing.T) { + helper := setupHelper(t) + namespace := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()) + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/metas", namespace) + response := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: "GET", + Path: path, + }, &pluginsv0alpha1.MetaList{}) + + require.NotNil(t, response.Result) + require.NotNil(t, response.Result.Items) + require.GreaterOrEqual(t, len(response.Result.Items), 0) + }) t.Run("get plugin meta", func(t *testing.T) { helper := setupHelper(t) @@ -35,12 +101,12 @@ func TestIntegrationPluginMeta(t *testing.T) { require.NoError(t, err) namespace := helper.Org1.Admin.Identity.GetNamespace() - path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas/%s", namespace, pluginName) + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/metas/%s", namespace, pluginName) response := apis.DoRequest(helper, apis.RequestParams{ User: helper.Org1.Admin, Method: "GET", Path: path, - }, &pluginsv0alpha1.PluginMeta{}) + }, &pluginsv0alpha1.Meta{}) require.NotNil(t, response.Result) require.NotNil(t, response.Result.Spec.PluginJSON) @@ -52,12 +118,12 @@ func TestIntegrationPluginMeta(t *testing.T) { t.Run("get plugin meta for non-existent plugin", func(t *testing.T) { helper := setupHelper(t) namespace := helper.Org1.Admin.Identity.GetNamespace() - path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas/non-existent-plugin", namespace) + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/metas/non-existent-plugin", namespace) response := apis.DoRequest(helper, apis.RequestParams{ User: helper.Org1.Admin, Method: "GET", Path: path, - }, &pluginsv0alpha1.PluginMeta{}) + }, &pluginsv0alpha1.Meta{}) require.NotNil(t, response.Status) require.Equal(t, int32(404), response.Status.Code) @@ -82,12 +148,12 @@ func TestIntegrationPluginMeta(t *testing.T) { require.NoError(t, err) namespace := helper.Org1.Admin.Identity.GetNamespace() - path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas/%s", namespace, pluginName) + path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/metas/%s", namespace, pluginName) response := apis.DoRequest(helper, apis.RequestParams{ User: helper.Org1.Admin, Method: "GET", Path: path, - }, &pluginsv0alpha1.PluginMeta{}) + }, &pluginsv0alpha1.Meta{}) require.NotNil(t, response.Status) require.Equal(t, int32(404), response.Status.Code) diff --git a/pkg/tests/apis/plugins/pluginmetas_test.go b/pkg/tests/apis/plugins/pluginmetas_test.go deleted file mode 100644 index 1ffe8d35a50..00000000000 --- a/pkg/tests/apis/plugins/pluginmetas_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package plugins - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" - "github.com/grafana/grafana/pkg/tests/apis" - "github.com/grafana/grafana/pkg/util/testutil" -) - -func TestIntegrationPluginMetas(t *testing.T) { - testutil.SkipIntegrationTestInShortMode(t) - - t.Run("list plugin metas", func(t *testing.T) { - helper := setupHelper(t) - ctx := context.Background() - client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - GVR: gvrPlugins, - }) - - plugin1Name := "test-plugin-metas-1" - plugin1 := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ - "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "Plugin", - "metadata": {"name": "%s"}, - "spec": {"id": "grafana-piechart-panel", "version": "1.0.0"} - }`, plugin1Name)) - _, err := client.Resource.Create(ctx, plugin1, metav1.CreateOptions{}) - require.NoError(t, err) - - plugin2Name := "test-plugin-metas-2" - plugin2 := helper.LoadYAMLOrJSON(fmt.Sprintf(`{ - "apiVersion": "plugins.grafana.app/v0alpha1", - "kind": "Plugin", - "metadata": {"name": "%s"}, - "spec": {"id": "grafana-clock-panel", "version": "1.0.0"} - }`, plugin2Name)) - _, err = client.Resource.Create(ctx, plugin2, metav1.CreateOptions{}) - require.NoError(t, err) - - namespace := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()) - path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas", namespace) - response := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: "GET", - Path: path, - }, &pluginsv0alpha1.PluginMetaList{}) - - require.NotNil(t, response.Result) - require.NotNil(t, response.Result.Items) - require.GreaterOrEqual(t, len(response.Result.Items), 2) - - foundIDs := make(map[string]bool) - for _, item := range response.Result.Items { - require.NotNil(t, item.Spec.PluginJSON) - foundIDs[item.Spec.PluginJSON.Id] = true - require.NotEmpty(t, item.Spec.PluginJSON.Id) - require.NotEmpty(t, item.Spec.PluginJSON.Type) - require.NotEmpty(t, item.Spec.PluginJSON.Name) - } - require.True(t, foundIDs["grafana-piechart-panel"]) - require.True(t, foundIDs["grafana-clock-panel"]) - }) - - t.Run("list plugin metas with no plugins", func(t *testing.T) { - helper := setupHelper(t) - namespace := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()) - path := fmt.Sprintf("/apis/plugins.grafana.app/v0alpha1/namespaces/%s/pluginmetas", namespace) - response := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: "GET", - Path: path, - }, &pluginsv0alpha1.PluginMetaList{}) - - require.NotNil(t, response.Result) - require.NotNil(t, response.Result.Items) - require.GreaterOrEqual(t, len(response.Result.Items), 0) - }) -} diff --git a/pkg/tests/apis/plugins/plugininstalls_test.go b/pkg/tests/apis/plugins/plugins_test.go similarity index 100% rename from pkg/tests/apis/plugins/plugininstalls_test.go rename to pkg/tests/apis/plugins/plugins_test.go diff --git a/public/app/core/icons/cached.json b/public/app/core/icons/cached.json index e78419e6d72..6e35e64dd0c 100644 --- a/public/app/core/icons/cached.json +++ b/public/app/core/icons/cached.json @@ -193,5 +193,6 @@ "unicons/ban", "unicons/git", "unicons/bitbucket", - "unicons/tachometer-fast" + "unicons/tachometer-fast", + "unicons/tachometer-empty" ] diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index ea232baffcc..2cbda7827e3 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -236,6 +236,10 @@ export class KeybindingSrv { appEvents.publish(new ZoomOutEvent({ scale: 0.5, updateUrl })); }); + this.bind('t =', () => { + appEvents.publish(new ZoomOutEvent({ scale: 0.5, updateUrl })); + }); + this.bind('t -', () => { appEvents.publish(new ZoomOutEvent({ scale: 2, updateUrl })); }); diff --git a/public/app/features/alerting/unified/CLAUDE.md b/public/app/features/alerting/unified/CLAUDE.md index 6fc487dd46d..124b9da622f 100644 --- a/public/app/features/alerting/unified/CLAUDE.md +++ b/public/app/features/alerting/unified/CLAUDE.md @@ -417,6 +417,60 @@ Check https://testing-library.com/docs/queries/about/ for what selectors to pref - [ ] Async operations use `await` and `findBy*` - [ ] Permissions tested with `grantUserPermissions` +## Using GitHub CLI for Context + +When working on issues, PRs, or needing repository context, use the GitHub CLI (`gh`) to fetch information directly: + +### Common Commands + +```bash +# View issue details +gh issue view + +# View PR details and diff +gh pr view +gh pr diff + +# List recent issues +gh issue list --limit 10 + +# List PRs with specific labels +gh pr list --label "alerting" + +# Search issues +gh issue list --search "keyword" + +# View PR reviews and comments +gh pr view --comments + +# Check CI status +gh pr checks + +# View repository info +gh repo view +``` + +### When to Use + +- **Understanding issue context**: Fetch issue descriptions, comments, and linked PRs +- **Reviewing PR changes**: Get diffs, review comments, and CI status +- **Finding related work**: Search for similar issues or existing implementations +- **Checking project status**: List open issues/PRs for the alerting team + +### Example Workflow + +```bash +# Working on issue #12345 +gh issue view 12345 + +# Check if there's an existing PR +gh pr list --search "fixes #12345" + +# Review a related PR +gh pr view 67890 +gh pr diff 67890 +``` + ## Getting Help - Check patterns in existing `components/` code @@ -425,6 +479,7 @@ Check https://testing-library.com/docs/queries/about/ for what selectors to pref - See `mocks.ts` for data factories - Read [./TESTING.md](./TESTING.md) for testing details - Review Grafana style guides (linked at top) +- Use `gh` CLI to fetch issue/PR context from GitHub --- diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index bb7708a5226..5a50ab55cd4 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -26,8 +26,10 @@ import { useStyles2, } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; +import { contextSrv } from 'app/core/services/context_srv'; import { ActiveTab as ContactPointsActiveTabs } from 'app/features/alerting/unified/components/contact-points/ContactPoints'; import { TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types'; +import { AccessControlAction } from 'app/types/accessControl'; import { AITemplateButtonComponent } from '../../enterprise-components/AI/AIGenTemplateButton/addAITemplateButton'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; @@ -100,6 +102,16 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) const formRef = useRef(null); const isGrafanaAlertManager = alertmanager === GRAFANA_RULES_SOURCE_NAME; + // Check if user has permission to test templates + const canTestTemplates = + contextSrv.hasPermission(AccessControlAction.AlertingNotificationsTemplatesTest) || + contextSrv.hasPermission(AccessControlAction.AlertingNotificationsWrite); + + // Only show preview and payload panels if both conditions are met: + // 1. It's a Grafana Alertmanager + // 2. User has the test permission + const showPreviewAndPayload = isGrafanaAlertManager && canTestTemplates; + const error = updateTemplateError ?? createTemplateError; const [cheatsheetOpened, toggleCheatsheetOpened] = useToggle(false); @@ -118,16 +130,16 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) // splitter for template and payload editor const columnSplitter = useSplitter({ direction: 'column', - // if Grafana Alertmanager, split 50/50, otherwise 100/0 because there is no payload editor - initialSize: isGrafanaAlertManager ? 0.5 : 1, + // if showing preview/payload panels, split 50/50, otherwise 100/0 because there is no payload editor + initialSize: showPreviewAndPayload ? 0.5 : 1, dragPosition: 'middle', }); // splitter for template editor and preview const rowSplitter = useSplitter({ direction: 'row', - // if Grafana Alertmanager, split 60/40, otherwise 100/0 because there is no preview - initialSize: isGrafanaAlertManager ? 0.6 : 1, + // if showing preview/payload panels, split 60/40, otherwise 100/0 because there is no preview + initialSize: showPreviewAndPayload ? 0.6 : 1, dragPosition: 'middle', }); @@ -319,8 +331,8 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) - {/* payload editor – only available for Grafana Alertmanager */} - {isGrafanaAlertManager && ( + {/* payload editor – only shown if user has test permission */} + {showPreviewAndPayload && ( <>
@@ -345,8 +357,8 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) )}
- {/* preview column – full height and half-width */} - {isGrafanaAlertManager && ( + {/* preview column – only shown if user has test permission */} + {showPreviewAndPayload && (
; + return ( + + ); } if (saveAsCopy || changeInfo.isNew) { diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index a542dd92826..d0fafd6f2bb 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -835,7 +835,7 @@ export class DashboardScene extends SceneObjectBase impleme kind: 'Dashboard', metadata: { ...meta.k8s, - name: meta.uid ?? meta.k8s?.name, + name: options.isNew ? undefined : (meta.uid ?? meta.k8s?.name), generateName: options.isNew ? 'd' : undefined, }, spec, diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts index 9a70858f39b..8772d49f138 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts @@ -269,6 +269,13 @@ describe('setupKeyboardShortcuts', () => { expect(tPlusBinding).toBeDefined(); }); + it('should setup t = zoom in shortcut', () => { + setupKeyboardShortcuts(mockScene); + + const tEqualsBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't ='); + expect(tEqualsBinding).toBeDefined(); + }); + it('should setup t - zoom out shortcut with keypress type', () => { setupKeyboardShortcuts(mockScene); @@ -302,9 +309,11 @@ describe('setupKeyboardShortcuts', () => { setupKeyboardShortcuts(mockScene); const tPlusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't +'); + const tEqualsBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't ='); const tMinusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't -'); expect(tPlusBinding).toBeUndefined(); + expect(tEqualsBinding).toBeUndefined(); expect(tMinusBinding).toBeUndefined(); }); }); @@ -364,6 +373,28 @@ describe('setupKeyboardShortcuts', () => { expect(newSpan).toBe(3 * 60 * 60 * 1000); // 3 hours in milliseconds }); + it('should zoom in (scale 0.5) when t = is pressed', () => { + setupKeyboardShortcuts(mockScene); + + const tEqualsBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't ='); + const handler = tEqualsBinding![0].onTrigger; + + handler(); + + // Scale 0.5 should result in 3 hour span (half of 6) + expect(mockTimeRange.onTimeRangeChange).toHaveBeenCalledWith( + expect.objectContaining({ + from: expect.any(Object), + to: expect.any(Object), + raw: expect.any(Object), + }) + ); + + const call = mockTimeRange.onTimeRangeChange.mock.calls[0][0]; + const newSpan = call.to.valueOf() - call.from.valueOf(); + expect(newSpan).toBe(3 * 60 * 60 * 1000); // 3 hours in milliseconds + }); + it('should keep center point when zooming in', () => { setupKeyboardShortcuts(mockScene); diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts index 1113b8a8a8c..f1878aeb466 100644 --- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts +++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts @@ -138,6 +138,13 @@ export function setupKeyboardShortcuts(scene: DashboardScene) { }, }); + keybindings.addBinding({ + key: 't =', + onTrigger: () => { + handleZoom(scene, 0.5); + }, + }); + keybindings.addBinding({ key: 't -', type: 'keypress', // NOTE: Because some browsers/OS identify minus symbol differently. diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index c17cc0cfbdb..aa2d81dcc52 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -330,10 +330,16 @@ describe('DashboardSceneSerializer', () => { { type: 'query', name: 'server', + datasource: { + type: 'influxdb', + }, }, { type: 'query', name: 'host', + datasource: { + type: 'elasticsearch', + }, }, { type: 'textbox', @@ -355,6 +361,10 @@ describe('DashboardSceneSerializer', () => { variable_type_textbox_count: 1, settings_nowdelay: undefined, settings_livenow: true, + varsWithDataSource: [ + { type: 'query', datasource: 'influxdb' }, + { type: 'query', datasource: 'elasticsearch' }, + ], }); }); }); @@ -688,16 +698,23 @@ describe('DashboardSceneSerializer', () => { schemaVersion: DASHBOARD_SCHEMA_VERSION, settings_nowdelay: undefined, settings_livenow: true, + panel_type_timeseries_count: 6, + variable_type_adhoc_count: 1, + variable_type_datasource_count: 1, variable_type_custom_count: 1, variable_type_query_count: 1, - panel_type_timeseries_count: 6, + varsWithDataSource: [ + { type: 'query', datasource: 'cloudwatch' }, + { type: 'adhoc', datasource: 'opensearch' }, + { type: 'datasource', datasource: 'bigquery' }, + ], }); expect(dashboard.getDynamicDashboardsTrackingInformation()).toEqual({ panelCount: 6, rowCount: 6, tabCount: 4, - templateVariableCount: 2, + templateVariableCount: 4, maxNestingLevel: 3, dashStructure: '[{"kind":"row","children":[{"kind":"row","children":[{"kind":"tab","children":[{"kind":"panel"},{"kind":"panel"},{"kind":"panel"}]},{"kind":"tab","children":[]}]},{"kind":"row","children":[{"kind":"row","children":[{"kind":"panel"}]}]}]},{"kind":"row","children":[{"kind":"row","children":[{"kind":"tab","children":[{"kind":"panel"}]},{"kind":"tab","children":[{"kind":"panel"}]}]}]}]', diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts index dda09ade2d4..cede8f605a5 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts @@ -178,7 +178,7 @@ export class V1DashboardSerializer uid: '', title: options.title || '', description: options.description || undefined, - tags: options.isNew || options.copyTags ? saveModel.tags : [], + tags: options.copyTags === false ? [] : saveModel.tags, }; } @@ -390,7 +390,7 @@ export class V2DashboardSerializer ...saveModel, title: options.title || '', description: options.description || '', - tags: options.isNew || options.copyTags ? saveModel.tags : [], + tags: options.copyTags === false ? [] : saveModel.tags, }; } diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap index 2a7f5bc424e..512ec28dd77 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap @@ -323,6 +323,7 @@ exports[`Given a scene with custom quick ranges should save quick ranges to save }, "refresh": 1, "regex": "", + "regexApplyTo": "value", "type": "query", }, { @@ -1049,6 +1050,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho }, "refresh": 1, "regex": "", + "regexApplyTo": "value", "type": "query", }, { @@ -1408,6 +1410,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr }, "refresh": 1, "regex": "", + "regexApplyTo": "value", "type": "query", }, { diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap index 603f84311fb..b0133a8eb92 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap @@ -173,6 +173,7 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model }, "refresh": "onDashboardLoad", "regex": "regex1", + "regexApplyTo": "value", "skipUrlSync": false, "sort": "alphabeticalDesc", }, diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts index cc49cfadc77..f45193f2ad8 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts @@ -141,6 +141,7 @@ describe('sceneVariablesSetToVariables', () => { "query": "query", "refresh": 1, "regex": "", + "regexApplyTo": "value", "staticOptions": [ { "text": "test", @@ -205,6 +206,7 @@ describe('sceneVariablesSetToVariables', () => { "query": "query", "refresh": 1, "regex": "", + "regexApplyTo": "value", "staticOptions": [ { "text": "test", @@ -1084,6 +1086,7 @@ describe('sceneVariablesSetToVariables', () => { }, "refresh": "onDashboardLoad", "regex": "", + "regexApplyTo": "value", "skipUrlSync": false, "sort": "disabled", "staticOptions": [ diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts index 87f6d650da7..466d659ab4b 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -84,6 +84,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio sort: variable.state.sort, refresh: variable.state.refresh, regex: variable.state.regex, + regexApplyTo: variable.state.regexApplyTo, allValue: variable.state.allValue, includeAll: variable.state.includeAll, multi: variable.state.isMulti, @@ -375,6 +376,7 @@ export function sceneVariablesSetToSchemaV2Variables( sort: transformSortVariableToEnum(variable.state.sort), refresh: transformVariableRefreshToEnum(variable.state.refresh), regex: variable.state.regex ?? '', + regexApplyTo: variable.state.regexApplyTo ?? 'value', allValue: variable.state.allValue, includeAll: variable.state.includeAll || false, multi: variable.state.isMulti || false, diff --git a/public/app/features/dashboard-scene/serialization/testfiles/nested_dashboard.json b/public/app/features/dashboard-scene/serialization/testfiles/nested_dashboard.json index 133bcd890ee..4322333ceb1 100644 --- a/public/app/features/dashboard-scene/serialization/testfiles/nested_dashboard.json +++ b/public/app/features/dashboard-scene/serialization/testfiles/nested_dashboard.json @@ -1132,6 +1132,14 @@ "sort": "disabled" } }, + { + "kind": "AdhocVariable", + "datasource": { "name": "esmce00tbim8" }, + "group": "opensearch", + "spec": { + "allowCustomValue": true + } + }, { "kind": "CustomVariable", "spec": { @@ -1154,6 +1162,13 @@ "query": "test", "skipUrlSync": false } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "datasourceVar", + "pluginId": "bigquery" + } } ] } diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 23a2b125aac..96781fe9fc2 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -366,6 +366,7 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S sort: transformSortVariableToEnumV1(variable.spec.sort), refresh: transformVariableRefreshToEnumV1(variable.spec.refresh), regex: variable.spec.regex, + regexApplyTo: variable.spec.regexApplyTo, allValue: variable.spec.allValue || undefined, includeAll: variable.spec.includeAll, defaultToAll: Boolean(variable.spec.includeAll), diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index f136ddc1998..6d0c450add3 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -283,6 +283,7 @@ describe('transformSceneToSaveModelSchemaV2', () => { sort: VariableSortV1.alphabeticalDesc, refresh: VariableRefresh.onDashboardLoad, regex: 'regex1', + regexApplyTo: 'value', allValue: '*', includeAll: true, isMulti: true, diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx index f14bfd9b176..9cbe786a05b 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx @@ -150,7 +150,12 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable const oldName = useRef(name); return ( - + { const mockOnQueryChange = jest.fn(); const mockOnLegacyQueryChange = jest.fn(); const mockOnRegExChange = jest.fn(); + const mockOnRegexApplyToChange = jest.fn(); const mockOnSortChange = jest.fn(); const mockOnRefreshChange = jest.fn(); const mockOnMultiChange = jest.fn(); @@ -89,6 +90,8 @@ describe('QueryVariableEditorForm', () => { timeRange: getDefaultTimeRange(), regex: '.*', onRegExChange: mockOnRegExChange, + regexApplyTo: 'value', + onRegexApplyToChange: mockOnRegexApplyToChange, sort: VariableSort.alphabeticalAsc, onSortChange: mockOnSortChange, refresh: VariableRefresh.onDashboardLoad, @@ -126,6 +129,9 @@ describe('QueryVariableEditorForm', () => { const regexInput = getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 ); + const regexApplyToSelect = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2 + ); const sortSelect = getByTestId( selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2 ); @@ -154,6 +160,8 @@ describe('QueryVariableEditorForm', () => { expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source'); expect(regexInput).toBeInTheDocument(); expect(regexInput).toHaveValue('.*'); + expect(regexApplyToSelect).toBeInTheDocument(); + expect(getByRole('radio', { name: 'Variable value' })).toBeChecked(); expect(sortSelect).toBeInTheDocument(); expect(sortSelect).toHaveTextContent('Alphabetical (asc)'); expect(refreshSelect).toBeInTheDocument(); @@ -213,6 +221,21 @@ describe('QueryVariableEditorForm', () => { ).toBe('.?'); }); + it('should call onRegexApplyToChange when selecting the regex apply to option', async () => { + const { + renderer: { getByTestId }, + } = await setup(); + const regexApplyToSelect = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2 + ); + await userEvent.click(regexApplyToSelect); + const anotherOption = screen.getByText('Display text'); + await userEvent.click(anotherOption); + + expect(mockOnRegexApplyToChange).toHaveBeenCalledTimes(1); + expect(mockOnRegexApplyToChange).toHaveBeenCalledWith('text'); + }); + it('should call onSortChange when changing the sort', async () => { const { renderer: { getByTestId }, diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx index 030de016a5f..b20f325d885 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx @@ -1,14 +1,15 @@ import { FormEvent, useCallback } from 'react'; import { useAsync } from 'react-use'; -import { DataSourceInstanceSettings, SelectableValue, TimeRange } from '@grafana/data'; +import { DataSourceInstanceSettings, SelectableValue, TimeRange, VariableRegexApplyTo } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { getDataSourceSrv } from '@grafana/runtime'; import { QueryVariable } from '@grafana/scenes'; import { DataSourceRef, VariableRefresh, VariableSort } from '@grafana/schema'; -import { Field, TextLink } from '@grafana/ui'; +import { Field } from '@grafana/ui'; import { QueryEditor } from 'app/features/dashboard-scene/settings/variables/components/QueryEditor'; +import { QueryVariableRegexForm } from 'app/features/dashboard-scene/settings/variables/components/QueryVariableRegexForm'; import { SelectionOptionsForm } from 'app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor'; @@ -21,7 +22,6 @@ import { } from 'app/features/variables/query/QueryVariableStaticOptions'; import { VariableLegend } from './VariableLegend'; -import { VariableTextAreaField } from './VariableTextAreaField'; type VariableQueryType = QueryVariable['state']['query']; @@ -34,6 +34,8 @@ interface QueryVariableEditorFormProps { timeRange: TimeRange; regex: string | null; onRegExChange: (event: FormEvent) => void; + regexApplyTo?: VariableRegexApplyTo; + onRegexApplyToChange?: (event: VariableRegexApplyTo) => void; sort: VariableSort; onSortChange: (option: SelectableValue) => void; refresh: VariableRefresh; @@ -61,6 +63,8 @@ export function QueryVariableEditorForm({ timeRange, regex, onRegExChange, + regexApplyTo, + onRegexApplyToChange, sort, onSortChange, refresh, @@ -131,32 +135,11 @@ export function QueryVariableEditorForm({ /> )} - - - Optional, if you want to extract part of a series name or metric node segment. - -
- - Named capture groups can be used to separate the display text and value ( - - see examples - - ). - -
- } - // eslint-disable-next-line @grafana/i18n/no-untranslated-strings - placeholder="/.*-(?.*)-(?.*)-.*/" - onBlur={onRegExChange} - testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2} - width={52} + { + const onRegExChange = jest.fn(); + const onRegexApplyToChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render the form fields correctly', () => { + const { getByTestId, getByRole } = render( + + ); + + const regexInput = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 + ); + const regexApplyToField = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2 + ); + + expect(regexInput).toBeInTheDocument(); + expect(regexInput).toHaveValue('.*test.*'); + expect(regexApplyToField).toBeInTheDocument(); + expect(getByRole('radio', { name: 'Variable value' })).toBeChecked(); + expect(getByRole('radio', { name: 'Display text' })).not.toBeChecked(); + }); + + it('should render with "Display text" option selected', () => { + const { getByRole } = render( + + ); + + expect(getByRole('radio', { name: 'Display text' })).toBeChecked(); + expect(getByRole('radio', { name: 'Variable value' })).not.toBeChecked(); + }); + + it('should default to "Variable value" when regexApplyTo is not provided', () => { + const { getByRole } = render( + + ); + + expect(getByRole('radio', { name: 'Variable value' })).toBeChecked(); + }); + + it('should call onRegExChange when regex input is blurred', () => { + const { getByTestId } = render( + + ); + + const regexInput = getByTestId( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 + ); + + fireEvent.blur(regexInput); + + expect(onRegExChange).toHaveBeenCalledTimes(1); + }); + + it('should call onRegexApplyToChange when radio option is changed', () => { + const { getByRole } = render( + + ); + + const displayTextOption = getByRole('radio', { name: 'Display text' }); + fireEvent.click(displayTextOption); + + expect(onRegexApplyToChange).toHaveBeenCalledTimes(1); + expect(onRegexApplyToChange).toHaveBeenCalledWith('text'); + }); +}); diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableRegexForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableRegexForm.tsx new file mode 100644 index 00000000000..dc72927c6fd --- /dev/null +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableRegexForm.tsx @@ -0,0 +1,91 @@ +import { useMemo, FormEvent } from 'react'; + +import { VariableRegexApplyTo, SelectableValue } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { Trans, t } from '@grafana/i18n'; +import { Field, Stack, TextLink, RadioButtonGroup, Box } from '@grafana/ui'; + +import { VariableTextAreaField } from '../components/VariableTextAreaField'; + +interface Props { + regex: string | null; + onRegExChange: (event: FormEvent) => void; + regexApplyTo?: VariableRegexApplyTo; + onRegexApplyToChange?: (option: VariableRegexApplyTo) => void; +} + +export function QueryVariableRegexForm({ regex, regexApplyTo, onRegExChange, onRegexApplyToChange }: Props) { + const APPLY_REGEX_TO_OPTIONS: Array> = useMemo( + () => [ + { + label: t('dashboard-scene.query-variable-editor-form.regex-apply-to-options.label.value', 'Variable value'), + value: 'value', + }, + { + label: t('dashboard-scene.query-variable-editor-form.regex-apply-to-options.label.text', 'Display text'), + value: 'text', + }, + ], + [] + ); + + const regexApplyToValue = useMemo( + () => APPLY_REGEX_TO_OPTIONS.find((o) => o.value === regexApplyTo)?.value ?? APPLY_REGEX_TO_OPTIONS[0].value, + [regexApplyTo, APPLY_REGEX_TO_OPTIONS] + ); + + return ( + + + + + Optional, if you want to extract part of a series name or metric node segment. + +
+ + Named capture groups can be used to separate the display text and value ( + + see examples + + ). + +
+ } + // eslint-disable-next-line @grafana/i18n/no-untranslated-strings + placeholder="/.*-(?.*)-(?.*)-.*/" + onBlur={onRegExChange} + testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2} + width={52} + noMargin + /> + + {onRegexApplyToChange && ( + + + + )} + + + ); +} diff --git a/public/app/features/dashboard-scene/settings/variables/components/VariableTextAreaField.tsx b/public/app/features/dashboard-scene/settings/variables/components/VariableTextAreaField.tsx index ae3e82ac525..260346c8d70 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/VariableTextAreaField.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/VariableTextAreaField.tsx @@ -1,7 +1,6 @@ import { css } from '@emotion/css'; import { useId } from '@react-aria/utils'; -import { FormEvent, PropsWithChildren, ReactElement } from 'react'; -import * as React from 'react'; +import { FormEvent, PropsWithChildren, ReactElement, ReactNode } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Field, TextArea, useStyles2 } from '@grafana/ui'; @@ -17,7 +16,8 @@ interface VariableTextAreaFieldProps { required?: boolean; testId?: string; onBlur?: (event: FormEvent) => void; - description?: React.ReactNode; + description?: ReactNode; + noMargin?: boolean; } export function VariableTextAreaField({ @@ -31,13 +31,14 @@ export function VariableTextAreaField({ ariaLabel, required, width, + noMargin, testId, }: PropsWithChildren): ReactElement { const styles = useStyles2(getStyles); const id = useId(); return ( - +