diff --git a/.betterer.eslint.config.js b/.betterer.eslint.config.js index 81b97bb03f3..77a920179e5 100644 --- a/.betterer.eslint.config.js +++ b/.betterer.eslint.config.js @@ -16,7 +16,7 @@ const grafanaI18nPlugin = require('@grafana/i18n/eslint-plugin'); // as we just want to pull in all of the necessary configuration but not run the rules // (this should only be concerned with checking rules that we want to improve, // so there's no need to try and run the rules that will be linted properly anyway) -const mappedBaseConfigs = grafanaConfig.map((config) => { +const mappedBaseConfigs = grafanaConfig.map((/** @type {import('eslint').Linter.Config} */ config) => { const { rules, ...baseConfig } = config; return baseConfig; }); diff --git a/.betterer.results b/.betterer.results index 398edbbc345..1a4c5338c06 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1534,6 +1534,13 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/rule-list/StateView.tsx:5381": [ [0, 0, 0, "Using localeCompare() can cause performance issues when sorting large datasets. Consider using Intl.Collator for better performance when sorting arrays, or add an eslint-disable comment if sorting a small, known dataset.", "0"] ], + "public/app/features/alerting/unified/types/alerting.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"] + ], "public/app/features/alerting/unified/types/receiver-form.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -1702,15 +1709,21 @@ exports[`better eslint`] = { [0, 0, 0, "React Hook \\"useSnappingSplitter\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "3"] ], "public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx:5381": [ - [0, 0, 0, "React Hook \\"useLayoutCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], - [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"] + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], + [0, 0, 0, "React Hook \\"useLayoutCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"] ], "public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx:5381": [ - [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], - [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"] + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "4"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "5"] ], "public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], @@ -1884,18 +1897,26 @@ exports[`better eslint`] = { [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "9"] ], "public/app/features/dashboard-scene/settings/variables/LocalVariableEditableElement.tsx:5381": [ - [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"] + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"] ], "public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], - [0, 0, 0, "React Hook \\"useLocalVariableOptions\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], - [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"], - [0, 0, 0, "React Hook \\"useVariableSelectionOptionsCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"], - [0, 0, 0, "React Hook \\"useVariableTypeCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "4"] + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "4"], + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "5"], + [0, 0, 0, "React Hook \\"useLocalVariableOptions\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "6"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "7"], + [0, 0, 0, "React Hook \\"useVariableSelectionOptionsCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "8"], + [0, 0, 0, "React Hook \\"useVariableTypeCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "9"] ], "public/app/features/dashboard-scene/settings/variables/VariableSetEditableElement.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], - [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"] + [0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"], + [0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"] ], "public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] @@ -3974,9 +3995,6 @@ exports[`better eslint`] = { "public/app/plugins/panel/table/cells/SparklineCellOptionsEditor.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], - "public/app/plugins/panel/table/cells/TextWrapOptionsEditor.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] - ], "public/app/plugins/panel/table/migrations.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -4053,13 +4071,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/types/alerting.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"] - ], "public/app/types/dashboard.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -4353,8 +4364,6 @@ exports[`no gf-form usage`] = { [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], - [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], "packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx:5381": [ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b00c55226b1..6124342bb89 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -969,7 +969,6 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform /public/app/routes/ @grafana/grafana-search-navigate-organise /public/app/store/ @grafana/grafana-frontend-platform /public/app/types/ @grafana/grafana-frontend-platform -/public/app/types/alerting.ts @grafana/alerting-frontend /public/app/types/unified-alerting-dto.ts @grafana/alerting-frontend /public/app/types/unified-alerting.ts @grafana/alerting-frontend /public/dashboards/ @grafana/dashboards-squad diff --git a/Dockerfile b/Dockerfile index 1c4e39f74fe..55ee94652a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -102,6 +102,7 @@ COPY apps/investigations apps/investigations COPY apps/advisor apps/advisor COPY apps/dashboard apps/dashboard COPY apps/folder apps/folder +COPY apps/preferences apps/preferences COPY apps/iam apps/iam COPY apps apps COPY kindsv2 kindsv2 diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 06c0acfa3a7..83d7ebb6489 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -25,7 +25,7 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler require ( cloud.google.com/go/compute/metadata v0.7.0 // indirect - dario.cat/mergo v1.0.1 // indirect + dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index a20488f5e2d..84b82dd6b94 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -72,8 +72,8 @@ cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2zn cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0= cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= diff --git a/apps/alerting/notifications/kinds/v0alpha1/receiver_spec.cue b/apps/alerting/notifications/kinds/v0alpha1/receiver_spec.cue index 66f78662617..8be089f8f61 100644 --- a/apps/alerting/notifications/kinds/v0alpha1/receiver_spec.cue +++ b/apps/alerting/notifications/kinds/v0alpha1/receiver_spec.cue @@ -8,6 +8,7 @@ ReceiverSpec: { #Integration: { uid?: string type: string + version: string disableResolveMessage?: bool settings: { [string]: _ diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_spec_gen.go b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_spec_gen.go index d43a3c51883..aaef41abfd5 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_spec_gen.go +++ b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_spec_gen.go @@ -6,6 +6,7 @@ package v0alpha1 type ReceiverIntegration struct { Uid *string `json:"uid,omitempty"` Type string `json:"type"` + Version string `json:"version"` DisableResolveMessage *bool `json:"disableResolveMessage,omitempty"` Settings map[string]interface{} `json:"settings"` SecureFields map[string]bool `json:"secureFields,omitempty"` diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/zz_openapi_gen.go b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/zz_openapi_gen.go index e79028361b2..5190f68f81d 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/zz_openapi_gen.go +++ b/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/zz_openapi_gen.go @@ -108,6 +108,13 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverIntegration(ref common.ReferenceC Format: "", }, }, + "version": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "disableResolveMessage": { SchemaProps: spec.SchemaProps{ Type: []string{"boolean"}, @@ -144,7 +151,7 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverIntegration(ref common.ReferenceC }, }, }, - Required: []string{"type", "settings"}, + Required: []string{"type", "version", "settings"}, }, }, } diff --git a/apps/dashboard/pkg/migration/schemaversion/v24.go b/apps/dashboard/pkg/migration/schemaversion/v24.go index 542938351f6..26e99569804 100644 --- a/apps/dashboard/pkg/migration/schemaversion/v24.go +++ b/apps/dashboard/pkg/migration/schemaversion/v24.go @@ -165,7 +165,7 @@ import ( // { // "matcher": { "id": "byName", "options": "Hidden" }, // "properties": [ -// { "id": "custom.hidden", "value": true } +// { "id": "custom.hideFrom.viz", "value": true } // ] // } // ] @@ -436,7 +436,7 @@ func migrateTableStyleToOverride(style map[string]interface{}) map[string]interf // Handle hidden type if styleType, ok := style["type"].(string); ok && styleType == "hidden" { properties = append(properties, map[string]interface{}{ - "id": "custom.hidden", + "id": "custom.hideFrom.viz", "value": true, }) } diff --git a/apps/dashboard/pkg/migration/schemaversion/v24_test.go b/apps/dashboard/pkg/migration/schemaversion/v24_test.go index ec093974fdb..dd258c9a0cd 100644 --- a/apps/dashboard/pkg/migration/schemaversion/v24_test.go +++ b/apps/dashboard/pkg/migration/schemaversion/v24_test.go @@ -216,7 +216,7 @@ func TestV24(t *testing.T) { "options": "Hidden", }, "properties": []interface{}{ - map[string]interface{}{"id": "custom.hidden", "value": true}, + map[string]interface{}{"id": "custom.hideFrom.viz", "value": true}, }, }, }, diff --git a/apps/dashboard/pkg/migration/testdata/output/v24.table-angular.json b/apps/dashboard/pkg/migration/testdata/output/v24.table-angular.json index 028723f153e..19f1b358b94 100644 --- a/apps/dashboard/pkg/migration/testdata/output/v24.table-angular.json +++ b/apps/dashboard/pkg/migration/testdata/output/v24.table-angular.json @@ -192,7 +192,7 @@ }, "properties": [ { - "id": "custom.hidden", + "id": "custom.hideFrom.viz", "value": true } ] diff --git a/apps/iam/.gitignore b/apps/iam/.gitignore new file mode 100644 index 00000000000..b7ebb479073 --- /dev/null +++ b/apps/iam/.gitignore @@ -0,0 +1,2 @@ +# Local development overrides +local/yamls/iam-secrets.yaml diff --git a/apps/iam/Makefile b/apps/iam/Makefile index 04746e30a31..71b00aca821 100644 --- a/apps/iam/Makefile +++ b/apps/iam/Makefile @@ -1,7 +1,5 @@ include ../sdk.mk -OPERATOR_DOCKERIMAGE := "github.com/grafana/grafana/apps/iam/operator" - .PHONY: generate generate: install-app-sdk update-app-sdk ## Run Grafana App SDK code generation @$(APP_SDK_BIN) generate \ @@ -17,17 +15,9 @@ deps: @go mod tidy @GOWORK=off go mod vendor -.PHONY: build -build: build/operator - -.PHONY: build/operator -build/operator: - docker build -t $(OPERATOR_DOCKERIMAGE) -f cmd/operator/Dockerfile . - .PHONY: local/up local/up: - @./local/scripts/cluster.sh create "local/k3d-config.json" - @cd local && tilt up + @tilt up .PHONY: local/generate local/generate: @@ -35,20 +25,4 @@ local/generate: .PHONY: local/down local/down: - @cd local && tilt down - -.PHONY: local/deploy_plugin -local/deploy_plugin: - -tilt disable grafana - cp -R plugin/dist local/mounted-files/plugin/dist - -tilt enable grafana - -.PHONY: local/push_operator -local/push_operator: build/operator - # Tag the docker image as part of localhost, which is what the generated k8s uses to avoid confusion with the real operator image - @docker tag "$(OPERATOR_DOCKERIMAGE):latest" "localhost/$(OPERATOR_DOCKERIMAGE):latest" - @./local/scripts/push_image.sh "localhost/$(OPERATOR_DOCKERIMAGE):latest" - -.PHONY: local/clean -local/clean: local/down - @./local/scripts/cluster.sh delete + @tilt down diff --git a/apps/iam/Tiltfile b/apps/iam/Tiltfile new file mode 100644 index 00000000000..f72dd50552a --- /dev/null +++ b/apps/iam/Tiltfile @@ -0,0 +1,16 @@ + +# version_settings() enforces a minimum Tilt version +# https://docs.tilt.dev/api.html#api.version_settings +version_settings(constraint='>=0.22.2') + +custom_build( + 'grafana-iam-operator', + command='docker build -t $EXPECTED_REF -f cmd/operator/Dockerfile .', + deps=[ + 'cmd/operator', + 'pkg', + ], + disable_push=True, +) + +k8s_yaml([filename for filename in listdir('local/yamls') if filename.lower().endswith(('.yaml', '.yml'))]) diff --git a/apps/iam/cmd/operator/config.go b/apps/iam/cmd/operator/config.go index 939399699c5..0794b3fd496 100644 --- a/apps/iam/cmd/operator/config.go +++ b/apps/iam/cmd/operator/config.go @@ -89,11 +89,11 @@ func LoadConfigFromEnv() (*Config, error) { } cfg.KubeConfig = kubeConfig } else if folderAppURL := os.Getenv("FOLDER_APP_URL"); folderAppURL != "" { - exchangeUrl := os.Getenv("AUTH_TOKEN_EXCHANGE_URL") + exchangeUrl := os.Getenv("TOKEN_EXCHANGE_URL") authToken := os.Getenv("AUTH_TOKEN") namespace := os.Getenv("FOLDER_APP_NAMESPACE") if exchangeUrl == "" || authToken == "" { - return nil, fmt.Errorf("AUTH_TOKEN_EXCHANGE_URL and AUTH_TOKEN must be set when FOLDER_APP_URL is set") + return nil, fmt.Errorf("TOKEN_EXCHANGE_URL and AUTH_TOKEN must be set when FOLDER_APP_URL is set") } kubeConfig, err := LoadKubeConfigFromFolderAppURL(folderAppURL, exchangeUrl, authToken, namespace) @@ -111,7 +111,7 @@ func LoadConfigFromEnv() (*Config, error) { cfg.ZanzanaClient.Address = os.Getenv("ZANZANA_ADDR") cfg.ZanzanaClient.Token = os.Getenv("ZANZANA_TOKEN") - cfg.ZanzanaClient.TokenExchangeURL = os.Getenv("ZANZANA_TOKEN_EXCHANGE_URL") + cfg.ZanzanaClient.TokenExchangeURL = os.Getenv("TOKEN_EXCHANGE_URL") cfg.ZanzanaClient.ServerCertFile = os.Getenv("ZANZANA_SERVER_CERT_FILE") cfg.FolderReconciler.Namespace = os.Getenv("FOLDER_RECONCILER_NAMESPACE") diff --git a/apps/iam/go.mod b/apps/iam/go.mod index c77608bdd55..26c9eb5a586 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -43,7 +43,7 @@ require ( cloud.google.com/go/monitoring v1.24.2 // indirect cloud.google.com/go/storage v1.55.0 // indirect cuelang.org/go v0.11.1 // indirect - dario.cat/mergo v1.0.1 // indirect + dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index fc611e8c1b2..dbec9ed4b1a 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -80,8 +80,8 @@ cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSB cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg= cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0= cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= diff --git a/apps/iam/kinds/user.cue b/apps/iam/kinds/user.cue index 480391dfa2e..a67c0949e1f 100644 --- a/apps/iam/kinds/user.cue +++ b/apps/iam/kinds/user.cue @@ -8,13 +8,41 @@ userKind: { kind: "User" pluralName: "Users" codegen: { - ts: { enabled: false } - go: { enabled: true } + ts: {enabled: false} + go: {enabled: true} } } userv0alpha1: userKind & { + // TODO: Uncomment this when User will be added to ManagedKinds + // validation: { + // operations: [ + // "CREATE", + // "UPDATE", + // ] + // } + // mutation: { + // operations: [ + // "CREATE", + // "UPDATE", + // ] + // } schema: { spec: v0alpha1.UserSpec } + // TODO: Uncomment when the custom routes implementation is done + // routes: { + // "/teams": { + // "GET": { + // response: { + // #UserTeam: { + // title: string + // teamRef: v0alpha1.TeamRef + // permission: v0alpha1.TeamPermission + // } + // items: [...#UserTeam] + // } + // } + // } + // } } diff --git a/apps/iam/kinds/v0alpha1/resourcepermission.cue b/apps/iam/kinds/v0alpha1/resourcepermission.cue index 5cb8c4de5d0..bbd901d284b 100644 --- a/apps/iam/kinds/v0alpha1/resourcepermission.cue +++ b/apps/iam/kinds/v0alpha1/resourcepermission.cue @@ -14,8 +14,8 @@ ResourcePermission: { kind: "User" | "ServiceAccount" | "Team" | "BasicRole" // uid of the identity getting the permission name: string - // list of actions granted to the user (e.g. "admin" or "get", "update") - verbs: [...string] + // action set granted to the user (e.g. "admin" or "edit", "view") + verb: string } resource: #Resource diff --git a/apps/iam/local/Tiltfile b/apps/iam/local/Tiltfile deleted file mode 100644 index 6c53501d811..00000000000 --- a/apps/iam/local/Tiltfile +++ /dev/null @@ -1,57 +0,0 @@ - -# version_settings() enforces a minimum Tilt version -# https://docs.tilt.dev/api.html#api.version_settings -version_settings(constraint='>=0.22.2') - -def name(c): - return c['metadata']['name'] - -def namespace(c): - if 'namespace' in c['metadata']: - return c['metadata']['namespace'] - return '' - -def decode(yaml): - resources = decode_yaml_stream(yaml) - - # workaround a bug in decode_yaml_stream where it returns duplicates - # This bug has been fixed in Tilt v0.17.3+ - filtered = [] - names = {} - for r in resources: - if r == None: - continue - - n = '%s:%s:%s' % (name(r), r['kind'], namespace(r)) - if n in names: - continue - - names[n] = True - filtered.append(r) - - return filtered - -def find_overlapping(o, yamls): - for elem in yamls: - if name(o) == name(elem) and o['kind'] == elem['kind'] and namespace(o) == namespace(elem): - return elem - return None - -yaml_objects = [] -# Parse all YAML files in our "yamls" directory -for filename in listdir('yamls'): - if filename.lower().endswith(('.yaml', '.yml')): - decoded = decode(read_file(filename)) - for o in decoded: - present = find_overlapping(o, yaml_objects) - if present != None: - print("Overlapping resource found: %s", filename) - exit(1) - yaml_objects += decoded - -bundle = encode_yaml_stream(yaml_objects) - -# k8s_yaml automatically creates resources in Tilt for the entities -# and will inject any images referenced in the Tiltfile when deploying -# https://docs.tilt.dev/api.html#api.k8s_yaml -k8s_yaml(bundle) diff --git a/apps/iam/local/k3d-config.json b/apps/iam/local/k3d-config.json deleted file mode 100644 index 224d6026fc7..00000000000 --- a/apps/iam/local/k3d-config.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "apiVersion": "k3d.io/v1alpha3", - "kind": "Simple", - "kubeAPI": { - "hostPort": "8556" - }, - "options": { - "k3d": { - "wait": true - }, - "kubeconfig": { - "switchCurrentContext": true, - "updateDefaultKubeconfig": true - } - } -} diff --git a/apps/iam/local/scripts/cluster.sh b/apps/iam/local/scripts/cluster.sh deleted file mode 100755 index 1edf4502d9c..00000000000 --- a/apps/iam/local/scripts/cluster.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash -set -eufo pipefail - -CLUSTER_NAME="grafana-iam-operator" - -create_cluster() { - K3D_CONFIG="${1:-k3d-config.json}" - - if ! k3d cluster list "${CLUSTER_NAME}" >/dev/null 2>&1; then - # Array of extra options to add to the k3d cluster create command - EXTRA_K3D_OPTS=() - - # Bug in k3d for btrfs filesystems workaround, see https://k3d.io/v5.2.2/faq/faq/#issues-with-btrfs - # Apple is APFS/HFS and stat has a different API, so we might as well skip that - if [[ "${OSTYPE}" != "darwin*" ]]; then - ROOTFS="$(stat -f --format="%T" "/")" - if [[ "${ROOTFS}" == "btrfs" ]]; then - EXTRA_K3D_OPTS+=("-v" "/dev/mapper:/dev/mapper") - fi - fi - - k3d cluster create "${CLUSTER_NAME}" --config "${K3D_CONFIG}" ${EXTRA_K3D_OPTS[@]+"${EXTRA_K3D_OPTS[@]}"} - else - echo "Cluster already exists" - fi -} - -delete_cluster() { - k3d cluster delete "${CLUSTER_NAME}" -} - -if [ $# -lt 1 ]; then - echo "Usage: ./cluster.sh [create|delete]" - exit 1 -fi - -if [ $1 == "create" ]; then - create_cluster $2 -elif [ $1 == "delete" ]; then - delete_cluster -else - echo "Unknown argument ${1}" -fi diff --git a/apps/iam/local/scripts/push_image.sh b/apps/iam/local/scripts/push_image.sh deleted file mode 100755 index 33257688764..00000000000 --- a/apps/iam/local/scripts/push_image.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -eufo pipefail - -CLUSTER_NAME="grafana-iam-operator" -IMAGE="$1" - -if [[ "$IMAGE" == "" ]]; then - echo "usage: push_image.sh " - exit 1 -fi - -k3d image import "${IMAGE}" -c "${CLUSTER_NAME}" \ No newline at end of file diff --git a/apps/iam/local/yamls/operator.yaml b/apps/iam/local/yamls/operator.yaml index 58fb79c68a5..4ef08bc7d19 100644 --- a/apps/iam/local/yamls/operator.yaml +++ b/apps/iam/local/yamls/operator.yaml @@ -23,7 +23,7 @@ spec: spec: serviceAccount: operator containers: - - image: localhost/github.com/grafana/grafana/apps/iam/operator:latest + - image: grafana-iam-operator imagePullPolicy: IfNotPresent name: iam-app-operator command: ["/bin/sh"] @@ -31,15 +31,28 @@ spec: - -c - exec /usr/bin/operator env: + - name: KUBE_FEATURE_WatchListClient + value: "false" - name: ZANZANA_ADDR value: zanzana.default.svc.cluster.local:50051 - name: FOLDER_APP_URL value: https://host.docker.internal:6446 - - name: AUTH_TOKEN_EXCHANGE_URL + - name: FOLDER_APP_NAMESPACE + value: grafana-folder + - name: TOKEN_EXCHANGE_URL value: http://host.docker.internal:8080/v1/sign-access-token - name: AUTH_TOKEN - value: "" - - name: FOLDER_APP_NAMESPACE - value: "grafana-folder" + valueFrom: + secretKeyRef: + name: iam-app-operator-secrets + key: auth-token + - name: ZANZANA_TOKEN + valueFrom: + secretKeyRef: + name: iam-app-operator-secrets + key: zanzana-token - name: FOLDER_RECONCILER_NAMESPACE - value: "default" + valueFrom: + secretKeyRef: + name: iam-app-operator-secrets + key: folder-reconciler-namespace diff --git a/apps/iam/local/yamls/zanzana.yaml b/apps/iam/local/yamls/zanzana.yaml index 095d7b58f27..5a306717468 100644 --- a/apps/iam/local/yamls/zanzana.yaml +++ b/apps/iam/local/yamls/zanzana.yaml @@ -88,12 +88,8 @@ metadata: name: zanzana data: grafana.ini: | - app_mode = development target = zanzana-server - [log] - level = info - [database] type = postgres host = postgres.default.svc:5432 @@ -103,17 +99,27 @@ data: [feature_toggles] zanzana = true - authZGRPCServer = true - - [zanzana.server] - allow_insecure = true - check_query_cache = true - http_addr = 0.0.0.0:8080 [grpc_server] enabled = true address = 0.0.0.0:50051 enable_logging = true + use_tls = false + + [log] + level = debug + + [server] + http_port = 8080 + + [zanzana.server] + check_query_cache = true + check_query_cache_enabled = true + check_query_cache_ttl = 10s + list_objects_deadline = 3s + list_objects_max_results = 1000 + signing_keys_url = http://host.docker.internal:8080/v1/keys + use_streamed_list_objects = false --- apiVersion: v1 kind: Service @@ -124,10 +130,10 @@ metadata: name: zanzana spec: ports: - - name: http + - name: zanzana-http-metrics port: 8080 targetPort: 8080 - - name: grpc + - name: zanzana-grpc port: 50051 targetPort: 50051 selector: @@ -159,19 +165,26 @@ spec: env: - name: GF_PATHS_CONFIG value: /etc/grafana-config/grafana.ini - image: grafana/grafana-oss-dev:12.2.0-257970 + image: grafana/grafana-dev:12.2.0-257970 imagePullPolicy: IfNotPresent name: zanzana + ports: + - containerPort: 50051 + name: grpc + - containerPort: 8080 + name: http-metrics + readinessProbe: + grpc: + port: 50051 + initialDelaySeconds: 10 + timeoutSeconds: 5 volumeMounts: - mountPath: /etc/grafana-config name: zanzana-config - - mountPath: /var/lib/grafana - name: zanzana-storage serviceAccount: zanzana volumes: - configMap: name: zanzana-config name: zanzana-config - - emptyDir: {} - name: zanzana-storage --- + diff --git a/apps/iam/pkg/apis/iam/v0alpha1/register.go b/apps/iam/pkg/apis/iam/v0alpha1/register.go index 3e0d4561290..cb1185c4264 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/register.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/register.go @@ -2,6 +2,7 @@ package v0alpha1 import ( "fmt" + "strings" "time" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -72,30 +73,202 @@ var RoleInfo = utils.NewResourceInfo(GROUP, VERSION, }, ) +var ResourcePermissionInfo = utils.NewResourceInfo(GROUP, VERSION, + "resourcepermissions", "resourcepermission", "ResourcePermission", + func() runtime.Object { return &ResourcePermission{} }, + func() runtime.Object { return &ResourcePermissionList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]interface{}, error) { + perm, ok := obj.(*ResourcePermission) + if ok { + if perm != nil { + return []interface{}{ + perm.Name, + perm.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + } + return nil, fmt.Errorf("expected resource permission") + }, + }, +) + +var userKind = UserKind() +var UserResourceInfo = utils.NewResourceInfo(userKind.Group(), userKind.Version(), + userKind.GroupVersionResource().Resource, strings.ToLower(userKind.Kind()), userKind.Kind(), + func() runtime.Object { return userKind.ZeroValue() }, + func() runtime.Object { return userKind.ZeroListValue() }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Login", Type: "string", Format: "string", Description: "The user login"}, + {Name: "Email", Type: "string", Format: "string", Description: "The user email"}, + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]interface{}, error) { + u, ok := obj.(*User) + if ok { + return []interface{}{ + u.Name, + u.Spec.Login, + u.Spec.Email, + u.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + return nil, fmt.Errorf("expected user") + }, + }, +) + +var teamKind = TeamKind() +var TeamResourceInfo = utils.NewResourceInfo(teamKind.Group(), teamKind.Version(), + teamKind.GroupVersionResource().Resource, strings.ToLower(teamKind.Kind()), teamKind.Kind(), + func() runtime.Object { return teamKind.ZeroValue() }, + func() runtime.Object { return teamKind.ZeroListValue() }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Title", Type: "string", Format: "string", Description: "The team name"}, + {Name: "Email", Type: "string", Format: "string", Description: "team email"}, + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]interface{}, error) { + m, ok := obj.(*Team) + if !ok { + return nil, fmt.Errorf("expected team") + } + return []interface{}{ + m.Name, + m.Spec.Title, + m.Spec.Email, + m.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + }, + }, +) + +var serviceAccountKind = ServiceAccountKind() +var ServiceAccountResourceInfo = utils.NewResourceInfo(serviceAccountKind.Group(), serviceAccountKind.Version(), + serviceAccountKind.GroupVersionResource().Resource, strings.ToLower(serviceAccountKind.Kind()), serviceAccountKind.Kind(), + func() runtime.Object { return serviceAccountKind.ZeroValue() }, + func() runtime.Object { return serviceAccountKind.ZeroListValue() }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Title", Type: "string", Format: "string"}, + {Name: "Disabled", Type: "boolean"}, + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]interface{}, error) { + sa, ok := obj.(*ServiceAccount) + if ok { + return []interface{}{ + sa.Name, + sa.Spec.Title, + sa.Spec.Disabled, + sa.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + return nil, fmt.Errorf("expected service account") + }, + }, +) + +var teamBindingKind = TeamBindingKind() +var TeamBindingResourceInfo = utils.NewResourceInfo( + teamBindingKind.Group(), teamBindingKind.Version(), + teamBindingKind.GroupVersionResource().Resource, + strings.ToLower(teamBindingKind.Kind()), teamBindingKind.Kind(), + func() runtime.Object { return teamBindingKind.ZeroValue() }, + func() runtime.Object { return teamBindingKind.ZeroListValue() }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Team", Type: "string"}, + {Name: "Created At", Type: "string", Format: "date"}, + }, + Reader: func(obj any) ([]interface{}, error) { + m, ok := obj.(*TeamBinding) + if !ok { + return nil, fmt.Errorf("expected team binding") + } + return []interface{}{ + m.Name, + m.Spec.TeamRef.Name, + m.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + }, + }, +) + var ( SchemeBuilder runtime.SchemeBuilder localSchemeBuilder = &SchemeBuilder AddToScheme = localSchemeBuilder.AddToScheme - schemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} + + SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} + InternalGroupVersion = schema.GroupVersion{Group: GROUP, Version: runtime.APIVersionInternal} ) func init() { - localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs) + localSchemeBuilder.Register(func(s *runtime.Scheme) error { + err := AddAuthZKnownTypes(s) + if err != nil { + return err + } + + err = AddAuthNKnownTypes(s) + if err != nil { + return err + } + + metav1.AddToGroupVersion(s, SchemeGroupVersion) + return nil + }, addDefaultingFuncs) } -// Adds the list of known types to the given scheme. -func addKnownTypes(scheme *runtime.Scheme) error { - scheme.AddKnownTypes(schemeGroupVersion, +func AddAuthZKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, &CoreRole{}, &CoreRoleList{}, &Role{}, &RoleList{}, + &ResourcePermission{}, + &ResourcePermissionList{}, // What is this about? &metav1.PartialObjectMetadata{}, &metav1.PartialObjectMetadataList{}, ) - metav1.AddToGroupVersion(scheme, schemeGroupVersion) + return nil +} + +func AddAuthNKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + // Identity + &User{}, + &UserList{}, + &ServiceAccount{}, + &ServiceAccountList{}, + &Team{}, + &TeamList{}, + &TeamBinding{}, + &TeamBindingList{}, + // For now these are registered in pkg/apis/iam/v0alpha1/register.go + // &UserTeamList{}, + // &ServiceAccountTokenList{}, + // &DisplayList{}, + // &SSOSetting{}, + // &SSOSettingList{}, + // &TeamMemberList{}, + + &metav1.PartialObjectMetadata{}, + &metav1.PartialObjectMetadataList{}, + ) return nil } diff --git a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_spec_gen.go index 57a17ac1bf9..192b5829798 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/resourcepermission_spec_gen.go @@ -23,15 +23,13 @@ type ResourcePermissionspecPermission struct { Kind ResourcePermissionSpecPermissionKind `json:"kind"` // uid of the identity getting the permission Name string `json:"name"` - // list of actions granted to the user (e.g. "admin" or "get", "update") - Verbs []string `json:"verbs"` + // action set granted to the user (e.g. "admin" or "edit", "view") + Verb string `json:"verb"` } // NewResourcePermissionspecPermission creates a new ResourcePermissionspecPermission object. func NewResourcePermissionspecPermission() *ResourcePermissionspecPermission { - return &ResourcePermissionspecPermission{ - Verbs: []string{}, - } + return &ResourcePermissionspecPermission{} } // +k8s:openapi-gen=true 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 7a0703d53e3..c8a1e09e6a6 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/zz_openapi_gen.go @@ -1111,23 +1111,16 @@ func schema_pkg_apis_iam_v0alpha1_ResourcePermissionspecPermission(ref common.Re Format: "", }, }, - "verbs": { + "verb": { SchemaProps: spec.SchemaProps{ - Description: "list of actions granted to the user (e.g. \"admin\" or \"get\", \"update\")", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, + Description: "action set granted to the user (e.g. \"admin\" or \"edit\", \"view\")", + Default: "", + Type: []string{"string"}, + Format: "", }, }, }, - Required: []string{"kind", "name", "verbs"}, + Required: []string{"kind", "name", "verb"}, }, }, } diff --git a/apps/iam/pkg/app/app.go b/apps/iam/pkg/app/app.go index 2da47c45575..ac63c7fb001 100644 --- a/apps/iam/pkg/app/app.go +++ b/apps/iam/pkg/app/app.go @@ -43,6 +43,12 @@ func New(cfg app.Config) (app.App, error) { logging.DefaultLogger.Info("FolderReconciler created") + reconcilerOptions := simple.BasicReconcileOptions{} + + if cfg.SpecificConfig.(AppConfig).FolderReconcilerNamespace != "" { + reconcilerOptions.Namespace = cfg.SpecificConfig.(AppConfig).FolderReconcilerNamespace + } + config := simple.AppConfig{ Name: cfg.ManifestData.AppName, KubeConfig: cfg.KubeConfig, @@ -54,11 +60,9 @@ func New(cfg app.Config) (app.App, error) { }, UnmanagedKinds: []simple.AppUnmanagedKind{ { - Kind: foldersKind.FolderKind(), - Reconciler: folderReconciler, - ReconcileOptions: simple.BasicReconcileOptions{ - Namespace: cfg.SpecificConfig.(AppConfig).FolderReconcilerNamespace, - }, + Kind: foldersKind.FolderKind(), + Reconciler: folderReconciler, + ReconcileOptions: reconcilerOptions, }, }, } diff --git a/apps/preferences/go.mod b/apps/preferences/go.mod index 63300e95d39..7dac6cc88e6 100644 --- a/apps/preferences/go.mod +++ b/apps/preferences/go.mod @@ -4,7 +4,9 @@ go 1.24.6 require ( github.com/grafana/grafana-app-sdk v0.40.3 + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 k8s.io/apimachinery v0.33.3 + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff ) require ( @@ -34,11 +36,13 @@ require ( github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // 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.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.10.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect @@ -54,7 +58,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/client-go v0.33.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/apps/preferences/go.sum b/apps/preferences/go.sum index 9ea34217f40..1a57d097497 100644 --- a/apps/preferences/go.sum +++ b/apps/preferences/go.sum @@ -36,6 +36,8 @@ github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhck github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk= github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE= github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= 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= diff --git a/apps/preferences/kinds/manifest.cue b/apps/preferences/kinds/manifest.cue index 3d7044dc1e8..aa75aa2d0ed 100644 --- a/apps/preferences/kinds/manifest.cue +++ b/apps/preferences/kinds/manifest.cue @@ -10,7 +10,8 @@ manifest: { go: {enabled: true} } kinds: [ - preferencesv1alpha1, + preferencesV1alpha1, + starsV1alpha1, ] } } diff --git a/apps/preferences/kinds/preferences.cue b/apps/preferences/kinds/preferences.cue index 579b8150642..6c39f5ef7ec 100644 --- a/apps/preferences/kinds/preferences.cue +++ b/apps/preferences/kinds/preferences.cue @@ -1,10 +1,16 @@ package preferences -preferencesv1alpha1: { +preferencesV1alpha1: { kind: "Preferences" pluralName: "Preferences" scope: "Namespaced" + validation: { + operations: [ + "CREATE", + "UPDATE", + ] + } schema: { spec: { // UID for the home dashboard diff --git a/apps/preferences/kinds/stars.cue b/apps/preferences/kinds/stars.cue new file mode 100644 index 00000000000..ede631b9fb0 --- /dev/null +++ b/apps/preferences/kinds/stars.cue @@ -0,0 +1,27 @@ +package preferences + +starsV1alpha1: { + kind: "Stars" + pluralName: "Stars" + scope: "Namespaced" + + validation: { + operations: [ + "CREATE", + "UPDATE", + ] + } + schema: { + #Resource: { + group: string + kind: string + + // The set of resources + // +listType=set + names: [...string] + } + spec: { + resource: [...#Resource] + } + } +} \ No newline at end of file diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/register.go b/apps/preferences/pkg/apis/preferences/v1alpha1/register.go new file mode 100644 index 00000000000..6bedc78a325 --- /dev/null +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/register.go @@ -0,0 +1,82 @@ +package v1alpha1 + +import ( + "fmt" + time "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +var PreferencesResourceInfo = utils.NewResourceInfo(APIGroup, APIVersion, + "preferences", "preferences", "Preferences", + func() runtime.Object { return &Preferences{} }, + func() runtime.Object { return &PreferencesList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]any, error) { + p, ok := obj.(*Preferences) + if ok && p != nil { + return []any{ + p.Name, + p.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + return nil, fmt.Errorf("expected preferences") + }, + }, +) + +var StarsResourceInfo = utils.NewResourceInfo(APIGroup, APIVersion, + "stars", "stars", "Stars", + func() runtime.Object { return &Stars{} }, + func() runtime.Object { return &StarsList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]any, error) { + p, ok := obj.(*Stars) + if ok && p != nil { + return []any{ + p.Name, + p.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + return nil, fmt.Errorf("expected stars") + }, + }, +) + +var ( + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme + schemeGroupVersion = GroupVersion +) + +func init() { + localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs) +} + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(schemeGroupVersion, + &Preferences{}, + &PreferencesList{}, + &Stars{}, + &StarsList{}, + ) + metav1.AddToGroupVersion(scheme, schemeGroupVersion) + return nil +} + +func addDefaultingFuncs(scheme *runtime.Scheme) error { + return nil // return RegisterDefaults(scheme) +} diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_codec_gen.go b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_codec_gen.go new file mode 100644 index 00000000000..85b4e19e128 --- /dev/null +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_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" +) + +// StarsJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type StarsJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*StarsJSONCodec) 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 (*StarsJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &StarsJSONCodec{} diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_metadata_gen.go b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_metadata_gen.go new file mode 100644 index 00000000000..4146bd0ecf0 --- /dev/null +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_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 StarsMetadata 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"` +} + +// NewStarsMetadata creates a new StarsMetadata object. +func NewStarsMetadata() *StarsMetadata { + return &StarsMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_object_gen.go b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_object_gen.go new file mode 100644 index 00000000000..d6836cea565 --- /dev/null +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_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 Stars struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the Stars + Spec StarsSpec `json:"spec" yaml:"spec"` + + Status StarsStatus `json:"status" yaml:"status"` +} + +func (o *Stars) GetSpec() any { + return o.Spec +} + +func (o *Stars) SetSpec(spec any) error { + cast, ok := spec.(StarsSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Stars) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *Stars) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *Stars) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(StarsStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type StarsStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *Stars) 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 *Stars) 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 *Stars) 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 *Stars) 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 *Stars) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Stars) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Stars) 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 *Stars) 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 *Stars) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Stars) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *Stars) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *Stars) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *Stars) DeepCopy() *Stars { + cpy := &Stars{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *Stars) DeepCopyInto(dst *Stars) { + 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 = &Stars{} + +// +k8s:openapi-gen=true +type StarsList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []Stars `json:"items" yaml:"items"` +} + +func (o *StarsList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *StarsList) Copy() resource.ListObject { + cpy := &StarsList{ + TypeMeta: o.TypeMeta, + Items: make([]Stars, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*Stars); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *StarsList) 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 *StarsList) SetItems(items []resource.Object) { + o.Items = make([]Stars, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*Stars) + } +} + +func (o *StarsList) DeepCopy() *StarsList { + cpy := &StarsList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *StarsList) DeepCopyInto(dst *StarsList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &StarsList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *StarsSpec) DeepCopy() *StarsSpec { + cpy := &StarsSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *StarsSpec) DeepCopyInto(dst *StarsSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of StarsStatus +func (s *StarsStatus) DeepCopy() *StarsStatus { + cpy := &StarsStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies StarsStatus into another StarsStatus object +func (s *StarsStatus) DeepCopyInto(dst *StarsStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_schema_gen.go b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_schema_gen.go new file mode 100644 index 00000000000..00e153b5ef2 --- /dev/null +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_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 ( + schemaStars = resource.NewSimpleSchema("preferences.grafana.app", "v1alpha1", &Stars{}, &StarsList{}, resource.WithKind("Stars"), + resource.WithPlural("stars"), resource.WithScope(resource.NamespacedScope)) + kindStars = resource.Kind{ + Schema: schemaStars, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &StarsJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func StarsKind() resource.Kind { + return kindStars +} + +// Schema returns a resource.SimpleSchema representation of Stars +func StarsSchema() *resource.SimpleSchema { + return schemaStars +} + +// Interface compliance checks +var _ resource.Schema = kindStars diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_spec_gen.go b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_spec_gen.go new file mode 100644 index 00000000000..b29475a6253 --- /dev/null +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_spec_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type StarsResource struct { + Group string `json:"group"` + Kind string `json:"kind"` + // The set of resources + // +listType=set + Names []string `json:"names"` +} + +// NewStarsResource creates a new StarsResource object. +func NewStarsResource() *StarsResource { + return &StarsResource{ + Names: []string{}, + } +} + +// +k8s:openapi-gen=true +type StarsSpec struct { + Resource []StarsResource `json:"resource"` +} + +// NewStarsSpec creates a new StarsSpec object. +func NewStarsSpec() *StarsSpec { + return &StarsSpec{ + Resource: []StarsResource{}, + } +} diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_status_gen.go b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_status_gen.go new file mode 100644 index 00000000000..c8c7c2b07f2 --- /dev/null +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/stars_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +// +k8s:openapi-gen=true +type StarsstatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State StarsStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewStarsstatusOperatorState creates a new StarsstatusOperatorState object. +func NewStarsstatusOperatorState() *StarsstatusOperatorState { + return &StarsstatusOperatorState{} +} + +// +k8s:openapi-gen=true +type StarsStatus struct { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]StarsstatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewStarsStatus creates a new StarsStatus object. +func NewStarsStatus() *StarsStatus { + return &StarsStatus{} +} + +// +k8s:openapi-gen=true +type StarsStatusOperatorStateState string + +const ( + StarsStatusOperatorStateStateSuccess StarsStatusOperatorStateState = "success" + StarsStatusOperatorStateStateInProgress StarsStatusOperatorStateState = "in_progress" + StarsStatusOperatorStateStateFailed StarsStatusOperatorStateState = "failed" +) diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi.go b/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi.go new file mode 100644 index 00000000000..7b9014a57bb --- /dev/null +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi.go @@ -0,0 +1,639 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by openapi-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + common "k8s.io/kube-openapi/pkg/common" + spec "k8s.io/kube-openapi/pkg/validation/spec" +) + +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{ + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Preferences": schema_pkg_apis_preferences_v1alpha1_Preferences(ref), + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesCookiePreferences": schema_pkg_apis_preferences_v1alpha1_PreferencesCookiePreferences(ref), + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesList": schema_pkg_apis_preferences_v1alpha1_PreferencesList(ref), + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesNavbarPreference": schema_pkg_apis_preferences_v1alpha1_PreferencesNavbarPreference(ref), + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesQueryHistoryPreference": schema_pkg_apis_preferences_v1alpha1_PreferencesQueryHistoryPreference(ref), + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec": schema_pkg_apis_preferences_v1alpha1_PreferencesSpec(ref), + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesStatus": schema_pkg_apis_preferences_v1alpha1_PreferencesStatus(ref), + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesstatusOperatorState": schema_pkg_apis_preferences_v1alpha1_PreferencesstatusOperatorState(ref), + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Stars": schema_pkg_apis_preferences_v1alpha1_Stars(ref), + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsList": schema_pkg_apis_preferences_v1alpha1_StarsList(ref), + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsResource": schema_pkg_apis_preferences_v1alpha1_StarsResource(ref), + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec": schema_pkg_apis_preferences_v1alpha1_StarsSpec(ref), + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsStatus": schema_pkg_apis_preferences_v1alpha1_StarsStatus(ref), + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsstatusOperatorState": schema_pkg_apis_preferences_v1alpha1_StarsstatusOperatorState(ref), + } +} + +func schema_pkg_apis_preferences_v1alpha1_Preferences(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the Preferences", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesStatus"), + }, + }, + }, + Required: []string{"metadata", "spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_preferences_v1alpha1_PreferencesCookiePreferences(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "analytics": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + "performance": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + "functional": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_preferences_v1alpha1_PreferencesList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Preferences"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Preferences", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_preferences_v1alpha1_PreferencesNavbarPreference(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "bookmarkUrls": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"bookmarkUrls"}, + }, + }, + } +} + +func schema_pkg_apis_preferences_v1alpha1_PreferencesQueryHistoryPreference(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "homeTab": { + SchemaProps: spec.SchemaProps{ + Description: "one of: '' | 'query' | 'starred';", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_preferences_v1alpha1_PreferencesSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "homeDashboardUID": { + SchemaProps: spec.SchemaProps{ + Description: "UID for the home dashboard", + Type: []string{"string"}, + Format: "", + }, + }, + "timezone": { + SchemaProps: spec.SchemaProps{ + Description: "The timezone selection", + Type: []string{"string"}, + Format: "", + }, + }, + "weekStart": { + SchemaProps: spec.SchemaProps{ + Description: "day of the week (sunday, monday, etc)", + Type: []string{"string"}, + Format: "", + }, + }, + "theme": { + SchemaProps: spec.SchemaProps{ + Description: "light, dark, empty is default", + Type: []string{"string"}, + Format: "", + }, + }, + "language": { + SchemaProps: spec.SchemaProps{ + Description: "Selected language (beta)", + Type: []string{"string"}, + Format: "", + }, + }, + "regionalFormat": { + SchemaProps: spec.SchemaProps{ + Description: "Selected locale (beta)", + Type: []string{"string"}, + Format: "", + }, + }, + "queryHistory": { + SchemaProps: spec.SchemaProps{ + Description: "Explore query history preferences", + Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesQueryHistoryPreference"), + }, + }, + "cookiePreferences": { + SchemaProps: spec.SchemaProps{ + Description: "Cookie preferences", + Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesCookiePreferences"), + }, + }, + "navbar": { + SchemaProps: spec.SchemaProps{ + Description: "Navigation preferences", + Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesNavbarPreference"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesCookiePreferences", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesNavbarPreference", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesQueryHistoryPreference"}, + } +} + +func schema_pkg_apis_preferences_v1alpha1_PreferencesStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "operatorStates": { + SchemaProps: spec.SchemaProps{ + Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesstatusOperatorState"), + }, + }, + }, + }, + }, + "additionalFields": { + SchemaProps: spec.SchemaProps{ + Description: "additionalFields is reserved for future use", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesstatusOperatorState"}, + } +} + +func schema_pkg_apis_preferences_v1alpha1_PreferencesstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "descriptiveState": { + SchemaProps: spec.SchemaProps{ + Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "details contains any extra information that is operator-specific", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"lastEvaluation", "state"}, + }, + }, + } +} + +func schema_pkg_apis_preferences_v1alpha1_Stars(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the Stars", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsStatus"), + }, + }, + }, + Required: []string{"metadata", "spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_preferences_v1alpha1_StarsList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Stars"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Stars", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_preferences_v1alpha1_StarsResource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "names": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "The set of resources", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"group", "kind", "names"}, + }, + }, + } +} + +func schema_pkg_apis_preferences_v1alpha1_StarsSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "resource": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsResource"), + }, + }, + }, + }, + }, + }, + Required: []string{"resource"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsResource"}, + } +} + +func schema_pkg_apis_preferences_v1alpha1_StarsStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "operatorStates": { + SchemaProps: spec.SchemaProps{ + Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsstatusOperatorState"), + }, + }, + }, + }, + }, + "additionalFields": { + SchemaProps: spec.SchemaProps{ + Description: "additionalFields is reserved for future use", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsstatusOperatorState"}, + } +} + +func schema_pkg_apis_preferences_v1alpha1_StarsstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "descriptiveState": { + SchemaProps: spec.SchemaProps{ + Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "details contains any extra information that is operator-specific", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"lastEvaluation", "state"}, + }, + }, + } +} diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi_violation_exceptions.list b/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi_violation_exceptions.list new file mode 100644 index 00000000000..80e3e004de7 --- /dev/null +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi_violation_exceptions.list @@ -0,0 +1,4 @@ +API rule violation: list_type_missing,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,PreferencesNavbarPreference,BookmarkUrls +API rule violation: list_type_missing,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,StarsSpec,Resource +API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,PreferencesList,ListMeta +API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,StarsList,ListMeta diff --git a/apps/preferences/pkg/apis/preferences_manifest.go b/apps/preferences/pkg/apis/preferences_manifest.go index 0b42a0e0713..b860ae2ab77 100644 --- a/apps/preferences/pkg/apis/preferences_manifest.go +++ b/apps/preferences/pkg/apis/preferences_manifest.go @@ -20,6 +20,9 @@ var ( rawSchemaPreferencesv1alpha1 = []byte(`{"spec":{"properties":{"cookiePreferences":{"description":"Cookie preferences","properties":{"analytics":{"type":"object"},"functional":{"type":"object"},"performance":{"type":"object"}},"type":"object"},"homeDashboardUID":{"description":"UID for the home dashboard","type":"string"},"language":{"description":"Selected language (beta)","type":"string"},"navbar":{"description":"Navigation preferences","properties":{"bookmarkUrls":{"items":{"type":"string"},"type":"array"}},"required":["bookmarkUrls"],"type":"object"},"queryHistory":{"description":"Explore query history preferences","properties":{"homeTab":{"description":"one of: '' | 'query' | 'starred';","type":"string"}},"type":"object"},"regionalFormat":{"description":"Selected locale (beta)","type":"string"},"theme":{"description":"light, dark, empty is default","type":"string"},"timezone":{"description":"The timezone selection\nTODO: this should use the timezone defined in common","type":"string"},"weekStart":{"description":"day of the week (sunday, monday, etc)","type":"string"}},"type":"object"},"status":{"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"}}`) versionSchemaPreferencesv1alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaPreferencesv1alpha1, &versionSchemaPreferencesv1alpha1) + rawSchemaStarsv1alpha1 = []byte(`{"spec":{"properties":{"resource":{"items":{"properties":{"group":{"type":"string"},"kind":{"type":"string"},"names":{"description":"The set of resources\n+listType=set","items":{"type":"string"},"type":"array"}},"required":["group","kind","names"],"type":"object"},"type":"array"}},"required":["resource"],"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"}}`) + versionSchemaStarsv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaStarsv1alpha1, &versionSchemaStarsv1alpha1) ) var appManifestData = app.ManifestData{ @@ -35,7 +38,31 @@ var appManifestData = app.ManifestData{ Plural: "Preferences", Scope: "Namespaced", Conversion: false, - Schema: &versionSchemaPreferencesv1alpha1, + Admission: &app.AdmissionCapabilities{ + Validation: &app.ValidationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + }, + Schema: &versionSchemaPreferencesv1alpha1, + }, + + { + Kind: "Stars", + Plural: "Stars", + Scope: "Namespaced", + Conversion: false, + Admission: &app.AdmissionCapabilities{ + Validation: &app.ValidationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + }, + Schema: &versionSchemaStarsv1alpha1, }, }, }, @@ -52,6 +79,7 @@ func RemoteManifest() app.Manifest { var kindVersionToGoType = map[string]resource.Kind{ "Preferences/v1alpha1": v1alpha1.PreferencesKind(), + "Stars/v1alpha1": v1alpha1.StarsKind(), } // ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. diff --git a/pkg/registry/apis/provisioning/apifmt/error.go b/apps/provisioning/pkg/apifmt/error.go similarity index 100% rename from pkg/registry/apis/provisioning/apifmt/error.go rename to apps/provisioning/pkg/apifmt/error.go diff --git a/pkg/registry/apis/provisioning/apifmt/error_test.go b/apps/provisioning/pkg/apifmt/error_test.go similarity index 97% rename from pkg/registry/apis/provisioning/apifmt/error_test.go rename to apps/provisioning/pkg/apifmt/error_test.go index b685bcad673..492494a2225 100644 --- a/pkg/registry/apis/provisioning/apifmt/error_test.go +++ b/apps/provisioning/pkg/apifmt/error_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/apifmt" + "github.com/grafana/grafana/apps/provisioning/pkg/apifmt" "github.com/stretchr/testify/assert" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/pkg/registry/apis/provisioning/loki/client.go b/apps/provisioning/pkg/loki/client.go similarity index 100% rename from pkg/registry/apis/provisioning/loki/client.go rename to apps/provisioning/pkg/loki/client.go diff --git a/pkg/registry/apis/provisioning/loki/client_test.go b/apps/provisioning/pkg/loki/client_test.go similarity index 100% rename from pkg/registry/apis/provisioning/loki/client_test.go rename to apps/provisioning/pkg/loki/client_test.go diff --git a/pkg/registry/apis/provisioning/safepath/dir.go b/apps/provisioning/pkg/safepath/dir.go similarity index 100% rename from pkg/registry/apis/provisioning/safepath/dir.go rename to apps/provisioning/pkg/safepath/dir.go diff --git a/pkg/registry/apis/provisioning/safepath/dir_test.go b/apps/provisioning/pkg/safepath/dir_test.go similarity index 100% rename from pkg/registry/apis/provisioning/safepath/dir_test.go rename to apps/provisioning/pkg/safepath/dir_test.go diff --git a/pkg/registry/apis/provisioning/safepath/path.go b/apps/provisioning/pkg/safepath/path.go similarity index 100% rename from pkg/registry/apis/provisioning/safepath/path.go rename to apps/provisioning/pkg/safepath/path.go diff --git a/pkg/registry/apis/provisioning/safepath/path_test.go b/apps/provisioning/pkg/safepath/path_test.go similarity index 100% rename from pkg/registry/apis/provisioning/safepath/path_test.go rename to apps/provisioning/pkg/safepath/path_test.go diff --git a/pkg/registry/apis/provisioning/safepath/safe.go b/apps/provisioning/pkg/safepath/safe.go similarity index 100% rename from pkg/registry/apis/provisioning/safepath/safe.go rename to apps/provisioning/pkg/safepath/safe.go diff --git a/pkg/registry/apis/provisioning/safepath/safe_test.go b/apps/provisioning/pkg/safepath/safe_test.go similarity index 100% rename from pkg/registry/apis/provisioning/safepath/safe_test.go rename to apps/provisioning/pkg/safepath/safe_test.go diff --git a/pkg/registry/apis/provisioning/safepath/trie.go b/apps/provisioning/pkg/safepath/trie.go similarity index 100% rename from pkg/registry/apis/provisioning/safepath/trie.go rename to apps/provisioning/pkg/safepath/trie.go diff --git a/pkg/registry/apis/provisioning/safepath/trie_test.go b/apps/provisioning/pkg/safepath/trie_test.go similarity index 100% rename from pkg/registry/apis/provisioning/safepath/trie_test.go rename to apps/provisioning/pkg/safepath/trie_test.go diff --git a/pkg/registry/apis/provisioning/safepath/walk.go b/apps/provisioning/pkg/safepath/walk.go similarity index 100% rename from pkg/registry/apis/provisioning/safepath/walk.go rename to apps/provisioning/pkg/safepath/walk.go diff --git a/pkg/registry/apis/provisioning/safepath/walk_test.go b/apps/provisioning/pkg/safepath/walk_test.go similarity index 100% rename from pkg/registry/apis/provisioning/safepath/walk_test.go rename to apps/provisioning/pkg/safepath/walk_test.go diff --git a/conf/defaults.ini b/conf/defaults.ini index c0f116e6a3e..3908ddf4ad4 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -2027,6 +2027,7 @@ enable = # This is EXPERIMENTAL. Please, do not use this section enable_api = true provider = static +url = [feature_toggles.openfeature.context] # This is EXPERIMENTAL. Please, do not use this section diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index 45805f71fb3..fc5ae74287a 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -224,57 +224,19 @@ make test-go-integration-postgres ### Run end-to-end tests -Grafana uses [Cypress](https://www.cypress.io/) to end-to-end test core features. Core plugins use [Playwright](https://playwright.dev/) to run automated end-to-end tests. You can find more information on how to add end-to-end tests to your core plugin [in our end-to-end testing style guide](./style-guides/e2e-plugins.md) +- Grafana uses [Playwright](https://playwright.dev/) to run automated end-to-end tests. You can find more information [in our end-to-end testing style guide](./style-guides/e2e-playwright.md#playwright-for-plugins) -#### Run Cypress tests +- Each version of Playwright needs specific versions of browser binaries to operate. You need to use the Playwright CLI to install these browsers: `yarn playwright install chromium`. +- Run tests with `yarn e2e:playwright [optional path to test file]`. -To run all tests in a headless Chromium browser. +- To open the last HTML report, you can run `yarn playwright show-report`. You can also open an arbitrary report with `yarn playwright show-report `. The reports are also downloadable from CI by: + - Clicking through to _End-to-end tests_/_All Playwright tests complete_. + - Clicking _Summary_. + - Download the _playwright-html-_ artifact. + - Unzip. + - Run `yarn playwright show-report ` -``` -yarn e2e -``` - -By default, the end-to-end tests start a Grafana instance listening on `localhost:3001`. To use a different URL, set the `BASE_URL` environment variable: - -``` -BASE_URL=http://localhost:3333 yarn e2e -``` - -To follow all tests in the browser while they're running, use `yarn e2e:debug` - -``` -yarn e2e:debug -``` - -To choose a single test to follow in the browser as it runs, use `yarn e2e:dev` - -``` -yarn e2e:dev -``` - -#### To run the Playwright tests: - -**Note:** If you're using VS Code as your development editor, it's recommended to install the [Playwright test extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). It allows you to run, debug and generate Playwright tests from within the editor. For more information about the extension and how to use reports to analyze failing tests, refer to the [Playwright documentation](https://playwright.dev/docs/getting-started-vscode). - -Each version of Playwright needs specific versions of browser binaries to operate. You need to use the Playwright CLI to install these browsers. - -``` -yarn playwright install chromium -``` - -The following script starts a Grafana [development server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) (same server that is being used when running e2e tests in CI) on port 3001 and runs all the Playwright tests. The development server is provisioned with the [devenv](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#add-data-sources) dashboards, data sources and apps. - -``` -yarn e2e:playwright -``` - -You can run against an arbitrary instance by setting the `GRAFANA_URL` environment variable: - -``` -GRAFANA_URL=http://localhost:3000 yarn e2e:playwright -``` - -Note this will not start a development server, so you must ensure that Grafana is running and accessible at the specified URL. +If you are curious about other commands, you can see the full list in [the Playwright documentation](https://playwright.dev/docs/test-cli#all-options). ## Configure Grafana for development diff --git a/contribute/style-guides/e2e-core.md b/contribute/style-guides/e2e-core.md deleted file mode 100644 index 60611e81f7c..00000000000 --- a/contribute/style-guides/e2e-core.md +++ /dev/null @@ -1,25 +0,0 @@ -# End-to-end tests for core Grafana - -This guide explains how to conduct end-to-end tests for the [Grafana repository](https://github.com/grafana/grafana). Ensure that you've read the [generalized E2E document](e2e.md). - -## Commands - -- `yarn e2e`: Creates an isolated `grafana-server` home under `\/e2e/tmp` with provisioned data sources and dashboards. This makes a local copy of the build binary and frontend assets from your repository root so you need to have a built backend and frontend. The server starts on port `3001` so it does not conflict with your normal dev server. -- `yarn e2e:debug`: Same as previous but runs the tests in Chrome and doesn't shut down after completion. -- `yarn e2e:dev`: Same as previous but does not run any tests on startup. It lets you pick a test first. - -If you already have a Grafana instance running, you can provide a specific URL by setting the `BASE_URL` environment variable: - -```shell -BASE_URL=http://172.0.10.2:3333 yarn e2e -``` - -The previous commands use some `utils` scripts under [_\/e2e_](../../e2e) that you can also use for more control. - -- `./scripts/grafana-server/start-server`: This creates a new Grafana server working directory, sets up configuration. and starts the server. It also kills any previously started server that is still running using the `pid` file at `\/scripts/grafana-server/tmp/pid`. -- `./scripts/grafana-server/wait-for-grafana`: waits for `$HOST` and `$PORT` to be available. Per default `localhost` and `3001`. -- `./e2e/run-suite `: Starts Cypress in different modes. - -## Test suites - -You can find integration tests at `\/e2e/suite\/specs`. diff --git a/contribute/style-guides/e2e-playwright.md b/contribute/style-guides/e2e-playwright.md new file mode 100644 index 00000000000..a746b6d675e --- /dev/null +++ b/contribute/style-guides/e2e-playwright.md @@ -0,0 +1,294 @@ +# End-to-end tests + +Grafana Labs uses a minimal [homegrown solution](https://github.com/grafana/plugin-tools/tree/main/packages/plugin-e2e) built on top of [Playwright](https://playwright.dev/) for its end-to-end (E2E) tests. + +Important notes: + +- We generally store all element identifiers ([CSS selectors](https://mdn.io/docs/Web/CSS/CSS_Selectors)) within the framework for reuse and maintainability. +- We generally do not use stubs or mocks as to fully simulate a real user. +- We also use Playwright for the [plugins' end-to-end tests](#playwright-for-plugins). + +## Framework structure + +Our framework structure is inspired by [Martin Fowler's Page Object](https://martinfowler.com/bliki/PageObject.html). + +- **`Selector`**: A unique identifier that is used from the E2E framework to retrieve an element from the browser +- **`Page`**: An abstraction for an object that contains one or more `Selector` identifiers with the `visit` function to go to the page. +- **`Component`**: An abstraction for an object that contains one or more `Selector` identifiers but without the `visit` function +- **`Flow`**: An abstraction that contains a sequence of actions on one or more `Page` abstractions that can be reused and shared between tests + +## How to run the Playwright tests: + +**Note:** If you're using VS Code as your development editor, it's recommended to install the [Playwright test extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). It allows you to run, debug and generate Playwright tests from within the editor. For more information about the extension and how to use reports to analyze failing tests, refer to the [Playwright documentation](https://playwright.dev/docs/getting-started-vscode). + +Each version of Playwright needs specific versions of browser binaries to operate. You need to use the Playwright CLI to install these browsers. + +``` +yarn playwright install chromium +``` + +The following script starts a Grafana [development server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) (same server that is being used when running e2e tests in CI) on port 3001 and runs all the Playwright tests. The development server is provisioned with the [devenv](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#add-data-sources) dashboards, data sources and apps. + +``` +yarn e2e:playwright +``` + +You can run against an arbitrary instance by setting the `GRAFANA_URL` environment variable: + +``` +GRAFANA_URL=http://localhost:3000 yarn e2e:playwright +``` + +Note this will not start a development server, so you must ensure that Grafana is running and accessible at the specified URL. + +### Commands commonly used: + +1 - **To open the Playwright UI**. It starts the Grafana server and then Playwright, which runs against this server. + +``` +yarn e2e:playwright --ui +``` + +2 - **To run an individual test**. It will run the test that matches the string passed to _grep_. Playwright will run all of them if you use a string that matches multiple tests. + +``` +yarn e2e:playwright --grep +``` + +You can also run all the tests matching a specific tag with _@tagName_. + +``` +yarn e2e:playwright --grep @ +``` + +3 - **To run a project**. It will run the entire project. You can find them in [grafana/playwright.config.ts](https://github.com/grafana/grafana/blob/main/playwright.config.ts#L90). + +``` +yarn e2e:playwright --project +``` + +4- **To open the last HTML report**. It will open a Chrome window with the test list and the related info (success/error, name, time, steps, ...). + +``` +yarn playwright show-report +``` + +You can open an arbitrary report with `yarn playwright show-report `. The reports are also downloadable from CI by: + +- Clicking through to _End-to-end tests_/_All Playwright tests complete_. +- Clicking _Summary_. +- Download the _playwright-html-_ artifact. +- Unzip. +- Run `yarn playwright show-report ` + +You can see the full list in [the Playwright documentation](https://playwright.dev/docs/test-cli#all-options) if you are curious about other commands. + +## How to create an end-to-end Playwright test + +### Basic example + +Let's start with a simple [JSX](https://reactjs.org/docs/introducing-jsx.html) example containing a single input field that we want to populate during our E2E test: + +```jsx + +``` + +It is possible to target the field with a CSS selector like `.login-form-input`. However, doing so is a brittle solution because style changes occur frequently. + +Furthermore, there is nothing that signals to future developers that this input is part of an E2E test. At Grafana, we use `data-testid` attributes as our preferred way of defining selectors. See [Aria-Labels vs data-testid](#aria-labels-vs-data-testid) for more details. + +```jsx + +``` + +The next step is to add the `Login` page to the `versionedPages` export within [_\/packages/grafana-e2e-selectors/src/selectors/pages.ts_](../../packages/grafana-e2e-selectors/src/selectors/pages.ts) so that it appears when we type `selectors.pages` in your IDE. + +```typescript +export const versionedPages = { + [...] + Login: { + url: { + [MIN_GRAFANA_VERSION]: '/login', + }, + username: { + '10.2.3': 'data-testid Username input field', + [MIN_GRAFANA_VERSION]: 'Username input field', + }, + password: { + '10.2.3': 'data-testid Password input field', + [MIN_GRAFANA_VERSION]: 'Password input field', + }, + submit: { + '10.2.3': 'data-testid Login button', + [MIN_GRAFANA_VERSION]: 'Login button', + }, + skip: { + '10.2.3': 'data-testid Skip change password button', + }, + }, +[...] +``` + +In this example, the username selector is prefixed with `data-testid` and specifies the minimum Grafana version for which the value is valid. The prefix is a signal to the framework to look for the selector in the `data-testid` attribute. + +Now that we have a page called `Login` in our `versionedPages` const, use it to add a selector in our HTML as shown in the following example. This page really signals to future developers that it is part of an E2E test. + +Example: + +```jsx +import { selectors } from '@grafana/e2e-selectors'; + +; +``` + +The last step in our example is to use our `Login` page as part of a test. + +- Use the `url` property whenever you call the [`page.goto()`](https://playwright.dev/docs/api/class-page#page-goto) in Playwright. +- Access any defined selector from the `Login` page by invoking it: [`page.getByTestId()`](https://playwright.dev/docs/api/class-page#page-get-by-test-id). + +```typescript +test( + 'Can login successfully', + { + tag: ['@acceptance'], + }, + async ({ selectors, page, grafanaAPICredentials }) => { + test.skip(grafanaAPICredentials.password === 'admin', 'Does not run with default password'); + + await page.goto(selectors.pages.Login.url); + + await page.getByTestId(selectors.pages.Login.username).fill(grafanaAPICredentials.user); + await page.getByTestId(selectors.pages.Login.password).fill(grafanaAPICredentials.password); + + await page.getByTestId(selectors.pages.Login.submit).click(); + + await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible(); + } +); +``` + +### Advanced example + +Let's take a look at an example that uses the same selector for multiple items in a list for instance. In this example app, there's a list of data sources that we want to click on during an E2E test. + +```jsx +
    + {dataSources.map(({ id, name }) => ( +
  • + +
    {name}
    +
    +
  • + ))} +
+``` + +Like in the basic example, start by creating a page abstraction: + +```typescript +export const versionedPages = { +[...] + DataSources: { + url: { + [MIN_GRAFANA_VERSION]: '/datasources', + }, + dataSources: { + [MIN_GRAFANA_VERSION]: (dataSourceName: string) => `Data source list item ${dataSourceName}`, + }, + }, +[...] +}; +``` + +You might have noticed that instead of a simple string as the selector, there's a function that takes a string parameter as an argument and returns a formatted string using the argument. + +Just as before, you need to add the `DataSources` url to the exported const `versionedPages` in `packages/grafana-e2e-selectors/src/selectors/pages.ts`. + +The next step is to use the `dataSources` selector function as in the following example: + +```jsx + +``` + +When this list is rendered with the data sources with names `A`, `B` and `C` ,the resulting HTML looks like this: + +```html +
A
+
B
+
C
+``` + +Now we can write our test. The one thing that differs from the previous [basic example](#basic-example) is that we pass in which data source we want to click as an argument to the selector function: + +```typescript +test( + 'List test clicks on data source named B', + { + tag: ['@datasources'], + }, + async ({ selectors, page }) => { + await page.goto(selectors.pages.Datasources.url); + // Select datasource + const dataSource = 'B'; + await page.getByTextId(dataSource).click(); + } +); +``` + +## Playwright for plugins + +When end-to-end testing Grafana plugins, a best practice is to use the [`@grafana/plugin-e2e`](https://www.npmjs.com/package/@grafana/plugin-e2e?activeTab=readme) testing tool. The `@grafana/plugin-e2e` tool extends [`@playwright/test`](https://playwright.dev/) capabilities with relevant fixtures, models, and expect matchers. Use it to enable comprehensive end-to-end testing of Grafana plugins across multiple versions of Grafana. + +> **Note:** To learn more, refer to our documentation on [plugin development](https://grafana.com/developers/plugin-tools/) and [end-to-end plugin testing](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/get-started). + +## Add end-to-end tests for a core plugin + +You can add Playwright end-to-end tests for plugins to the [`e2e-playwright/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e) directory. + +1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e). This is the directory where your plugin tests will be kept. + +1. Playwright uses [projects](https://playwright.dev/docs/test-projects) to logically group tests together. All tests in a project share the same configuration. + In the [Playwright config file](https://github.com/grafana/grafana/blob/main/playwright.config.ts), add a new project item. Make sure the `name` and the `testDir` subdirectory match the name of the directory that contains your plugin tests. + Add `'authenticate'` to the list of dependencies and specify `'playwright/.auth/admin.json'` as the storage state to ensure that all tests in your project will start already authenticated as an admin user. If you want to use a different role for and perhaps test RBAC for some of your tests, refer to our [documentation](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/use-authentication). + + ```ts + { + name: 'mysql', + testDir: path.join(testDirRoot, '/mysql'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + ``` + +1. Update the [CODEOWNERS](https://github.com/grafana/grafana/blob/main/.github/CODEOWNERS/#L315) file so that your team is owner of the tests in the directory you added in step 1. + +## Commands + +- `yarn e2e:playwright` runs all Playwright tests. Optionally, you can provide the `--project mysql` argument to run tests in a specific project. + + The `yarn e2e:playwright` command starts a Grafana [development server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) on port 3001 and runs the Playwright tests. + + You can run against an arbitrary instance by setting the `GRAFANA_URL` environment variable: + + `GRAFANA_URL=http://localhost:3000 yarn e2e:playwright` + + Note this will not start a development server, so you must ensure that Grafana is running and accessible at the specified URL. + +- You can provision the development server with the [devenv](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#add-data-sources) dashboards, data sources, and apps. + +> ### Playwright tests in Grafana Enterprise +> +> We are currently working on make Playwright available for creating end-to-end tests in [grafana-enterprise](https://github.com/grafana/grafana-enterprise). diff --git a/contribute/style-guides/e2e-plugins.md b/contribute/style-guides/e2e-plugins.md deleted file mode 100644 index e3fafa36fcf..00000000000 --- a/contribute/style-guides/e2e-plugins.md +++ /dev/null @@ -1,43 +0,0 @@ -# End-to-end tests for plugins - -When end-to-end testing Grafana plugins, a best practice is to use the [`@grafana/plugin-e2e`](https://www.npmjs.com/package/@grafana/plugin-e2e?activeTab=readme) testing tool. The `@grafana/plugin-e2e` tool extends [`@playwright/test`](https://playwright.dev/) capabilities with relevant fixtures, models, and expect matchers. Use it to enable comprehensive end-to-end testing of Grafana plugins across multiple versions of Grafana. - -> **Note:** To learn more, refer to our documentation on [plugin development](https://grafana.com/developers/plugin-tools/) and [end-to-end plugin testing](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/get-started). - -## Add end-to-end tests for a core plugin - -You can add Playwright end-to-end tests for plugins to the [`e2e-playwright/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e) directory. - -1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e). This is the directory where your plugin tests will be kept. - -1. Playwright uses [projects](https://playwright.dev/docs/test-projects) to logically group tests together. All tests in a project share the same configuration. - In the [Playwright config file](https://github.com/grafana/grafana/blob/main/playwright.config.ts), add a new project item. Make sure the `name` and the `testDir` subdirectory match the name of the directory that contains your plugin tests. - Add `'authenticate'` to the list of dependencies and specify `'playwright/.auth/admin.json'` as the storage state to ensure that all tests in your project will start already authenticated as an admin user. If you want to use a different role for and perhaps test RBAC for some of your tests, refer to our [documentation](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/use-authentication). - - ```ts - { - name: 'mysql', - testDir: path.join(testDirRoot, '/mysql'), - use: { - ...devices['Desktop Chrome'], - storageState: 'playwright/.auth/admin.json', - }, - dependencies: ['authenticate'], - }, - ``` - -1. Update the [CODEOWNERS](https://github.com/grafana/grafana/blob/main/.github/CODEOWNERS/#L315) file so that your team is owner of the tests in the directory you added in step 1. - -## Commands - -- `yarn e2e:playwright` runs all Playwright tests. Optionally, you can provide the `--project mysql` argument to run tests in a specific project. - - The `yarn e2e:playwright` command starts a Grafana [development server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) on port 3001 and runs the Playwright tests. - - You can run against an arbitrary instance by setting the `GRAFANA_URL` environment variable: - - `GRAFANA_URL=http://localhost:3000 yarn e2e:playwright` - - Note this will not start a development server, so you must ensure that Grafana is running and accessible at the specified URL. - -- You can provision the development server with the [devenv](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#add-data-sources) dashboards, data sources, and apps. diff --git a/contribute/style-guides/e2e.md b/contribute/style-guides/e2e.md deleted file mode 100644 index 89424bafd9e..00000000000 --- a/contribute/style-guides/e2e.md +++ /dev/null @@ -1,194 +0,0 @@ -# End-to-end tests - -Grafana Labs uses a minimal [homegrown solution](../../e2e/utils/index.ts) built on top of [Cypress](https://cypress.io) for its end-to-end (E2E) tests. - -Important notes: - -- We generally store all element identifiers ([CSS selectors](https://mdn.io/docs/Web/CSS/CSS_Selectors)) within the framework for reuse and maintainability. -- We generally do not use stubs or mocks as to fully simulate a real user. -- Cypress' promises [don't behave as you might expect](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Mixing-Async-and-Sync-code). -- [Testing core Grafana](e2e-core.md) is different than [testing plugins](e2e-plugins.md)—core Grafana uses Cypress whereas plugins use [Playwright test](https://playwright.dev/). - -## Framework structure - -Our framework structure is inspired by [Martin Fowler's Page Object](https://martinfowler.com/bliki/PageObject.html). - -- **`Selector`**: A unique identifier that is used from the E2E framework to retrieve an element from the browser -- **`Page`**: An abstraction for an object that contains one or more `Selector` identifiers with the `visit` function to go to the page. -- **`Component`**: An abstraction for an object that contains one or more `Selector` identifiers but without the `visit` function -- **`Flow`**: An abstraction that contains a sequence of actions on one or more `Page` abstractions that can be reused and shared between tests - -## Basic example - -Let's start with a simple [JSX](https://reactjs.org/docs/introducing-jsx.html) example containing a single input field that we want to populate during our E2E test: - -```jsx - -``` - -It is possible to target the field with a CSS selector like `.gf-form-input.login-form-input`. However, doing so is a brittle solution because style changes occur frequently. - -Furthermore, there is nothing that signals to future developers that this input is part of an E2E test. At Grafana, we use `data-testid` attributes as our preferred way of defining selectors. See [Aria-Labels vs data-testid](#aria-labels-vs-data-testid) for more details. - -```jsx - -``` - -The next step is to create a `Page` representation in our E2E framework. Doing so glues the test with the real implementation using the `pageFactory` function. For that function we can supply a `url` and selector like in the following example: - -```typescript -export const Login = { - // Called via `Login.visit()` - url: '/login', - // Called via `Login.username()` - username: 'data-testid Username input field', -}; -``` - -In this example, the selector is prefixed with `data-testid`. The prefix is a signal to the framework to look for the selector in the `data-testid` attribute. - -The next step is to add the `Login` page to the `Pages` export within [_\/packages/grafana-e2e-selectors/src/selectors/pages.ts_](../../packages/grafana-e2e-selectors/src/selectors/pages.ts) so that it appears when we type `e2e.pages` in your IDE. - -```typescript -export const Pages = { - Login, - …, - …, - …, -}; -``` - -Now that we have a page called `Login` in our `Pages` const, use it to add a selector in our HTML as shown in the following example. This page really signals to future developers that it is part of an E2E test. - -Example: - -```jsx -import { selectors } from '@grafana/e2e-selectors'; - -; -``` - -The last step in our example is to use our `Login` page as part of a test. - -- Use the `url` property whenever you call the `visit` function. It is equivalent to the [`cy.visit()`](https://docs.cypress.io/api/commands/visit.html#Syntax) in Cypress. -- Access any defined selector from the `Login` page by invoking it. This is equivalent to the result of the Cypress function [`cy.get(…)`](https://docs.cypress.io/api/commands/get.html#Syntax). - -```typescript -describe('Login test', () => { - it('passes', () => { - e2e.pages.Login.visit(); - // To prevent flaky tests, always do a `.should` on any selector that you expect to be in the DOM. - // Read more here: https://docs.cypress.io/guides/core-concepts/retry-ability.html#Commands-vs-assertions - e2e.pages.Login.username().should('be.visible').type('admin'); - }); -}); -``` - -## Advanced example - -Let's take a look at an example that uses the same selector for multiple items in a list for instance. In this example app, there's a list of data sources that we want to click on during an E2E test. - -```jsx -
    - {dataSources.map(({ id, name }) => ( -
  • - -
    {name}
    -
    -
  • - ))} -
-``` - -Like in the basic example, start by creating a page abstraction using the `pageFactory` function: - -```typescript -export const DataSources = { - url: '/datasources', - dataSources: (dataSourceName: string) => `data-testid Data source list item ${dataSourceName}`, -}; -``` - -You might have noticed that instead of a simple string as the selector, there's a function that takes a string parameter as an argument and returns a formatted string using the argument. - -Just as before, you need to add the `DataSources` page to the exported const `Pages` in `packages/grafana-e2e-selectors/src/selectors/pages.ts`. - -The next step is to use the `dataSources` selector function as in the following example: - -```jsx - -``` - -When this list is rendered with the data sources with names `A`, `B` and `C` ,the resulting HTML looks like this: - -```html -
A
-
B
-
C
-``` - -Now we can write our test. The one thing that differs from the previous [basic example](#basic-example) is that we pass in which data source we want to click as an argument to the selector function: - -```typescript -describe('List test', () => { - it('clicks on data source named B', () => { - e2e.pages.DataSources.visit(); - // To prevent flaky tests, always do a .should on any selector that you expect to be in the DOM. - // Read more here: https://docs.cypress.io/guides/core-concepts/retry-ability.html#Commands-vs-assertions - e2e.pages.DataSources.dataSources('B').should('be.visible').click(); - }); -}); -``` - -## aria-label versus data-testid - -Our selectors are set up to work with both `aria-label` attributes and `data-testid` attributes. The `aria-label` attributes help assistive technologies such as screen readers identify interactive elements of a page for our users. - -A good example of a time to use an aria-label might be if you have a button with an **X** to close: - -``` -