diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c7339cdb3a9..a5360c3969a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -310,6 +310,7 @@ /devenv/docker/blocks/webdav/ @grafana/alerting-backend /devenv/docker/buildcontainer/ @bergquist /devenv/docker/compose_header.yml @grafana/grafana-backend-services-squad +/devenv/docker/compose_volume_section.yml @grafana/grafana-backend-services-squad /devenv/docker/debtest/ @bergquist /devenv/docker/ha-test-unified-alerting/ @grafana/alerting-backend /devenv/docker/ha_test/ @grafana/grafana-backend-services-squad @@ -926,6 +927,7 @@ playwright.storybook.config.ts @grafana/grafana-frontend-platform /public/app/features/inspector/ @grafana/dashboards-squad /public/app/features/logs/ @grafana/observability-logs /public/app/features/live/ @grafana/dashboards-squad +/public/app/features/stars/ @grafana/grafana-search-navigate-organise /public/app/features/apiserver/ @grafana/grafana-app-platform-squad /public/app/features/manage-dashboards/ @grafana/dashboards-squad /public/app/features/migrate-to-cloud @grafana/grafana-operator-experience-squad diff --git a/.github/workflows/pr-go-workspace-check.yml b/.github/workflows/pr-go-workspace-check.yml index 5e826221a33..78a2e761ebc 100644 --- a/.github/workflows/pr-go-workspace-check.yml +++ b/.github/workflows/pr-go-workspace-check.yml @@ -123,7 +123,7 @@ jobs: echo "No changes in generated Go files" else if [[ "${{ github.event.pull_request.head.repo.fork }}" == "false" ]]; then - echo "> !!! Please link Enterprise and run 'make gen-go', then commit the changes to both repositories." + echo "> !!! Please synchronize the grafana OSS and grafana enterprise code bases as defined in the enterprise readme, then run 'make gen-go' in the OSS folder and commit the changes to both repositories." else echo "> !!! Please run 'make gen-go' and commit the changes." fi diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml index 9caeaebe252..e4bed54a7f8 100644 --- a/.github/workflows/release-npm.yml +++ b/.github/workflows/release-npm.yml @@ -135,7 +135,6 @@ jobs: tag-nightly: name: Tag nightly release runs-on: github-hosted-ubuntu-x64-small - needs: publish if: inputs.version_type == 'nightly' steps: @@ -146,4 +145,3 @@ jobs: # TODO: tag the given release with nightly - diff --git a/apps/alerting/notifications/Makefile b/apps/alerting/notifications/Makefile index 817a6d508ff..039a81a6f6e 100644 --- a/apps/alerting/notifications/Makefile +++ b/apps/alerting/notifications/Makefile @@ -5,5 +5,4 @@ generate: do-generate ## Run Grafana App SDK code generation .PHONY: do-generate do-generate: install-app-sdk update-app-sdk - ## --defencoding=none and noschemasinmanifest are needed to avoid infinite loop while generating recursive models (see routingtree.cue) - @$(APP_SDK_BIN) generate --grouping=group --gogenpath=./pkg/apis --defencoding=none --postprocess --noschemasinmanifest --useoldmanifestkinds + @$(APP_SDK_BIN) generate --grouping=group --gogenpath=./pkg/apis --defencoding=yaml --postprocess diff --git a/apps/alerting/notifications/definitions/alerting-notifications-manifest.yaml b/apps/alerting/notifications/definitions/alerting-notifications-manifest.yaml new file mode 100644 index 00000000000..447c5d5f747 --- /dev/null +++ b/apps/alerting/notifications/definitions/alerting-notifications-manifest.yaml @@ -0,0 +1,432 @@ +apiVersion: apps.grafana.com/v1alpha2 +kind: AppManifest +metadata: + name: alerting-notifications +spec: + appName: alerting-notifications + group: notifications.alerting.grafana.app + versions: + - kinds: + - conversion: false + kind: Receiver + plural: Receivers + schemas: + Integration: + additionalProperties: false + properties: + disableResolveMessage: + type: boolean + secureFields: + additionalProperties: + type: boolean + type: object + settings: + additionalProperties: + additionalProperties: {} + type: object + type: object + type: + type: string + uid: + type: string + version: + type: string + required: + - type + - version + - settings + type: object + OperatorState: + additionalProperties: false + properties: + descriptiveState: + description: descriptiveState is an optional more descriptive state + field which has no requirements on format + type: string + details: + additionalProperties: + additionalProperties: {} + type: object + description: details contains any extra information that is operator-specific + type: object + lastEvaluation: + description: lastEvaluation is the ResourceVersion last evaluated + type: string + state: + description: |- + state describes the state of the lastEvaluation. + It is limited to three possible states for machine evaluation. + enum: + - success + - in_progress + - failed + type: string + required: + - lastEvaluation + - state + type: object + Receiver: + properties: + spec: + $ref: '#/components/schemas/spec' + status: + $ref: '#/components/schemas/status' + required: + - spec + spec: + additionalProperties: false + properties: + integrations: + items: + $ref: '#/components/schemas/Integration' + type: array + title: + type: string + required: + - title + - integrations + type: object + status: + additionalProperties: false + properties: + additionalFields: + additionalProperties: + additionalProperties: {} + type: object + description: additionalFields is reserved for future use + type: object + operatorStates: + additionalProperties: + $ref: '#/components/schemas/OperatorState' + description: |- + operatorStates is a map of operator ID to operator state evaluations. + Any operator which consumes this kind SHOULD add its state evaluation information to this field. + type: object + type: object + scope: Namespaced + - conversion: false + kind: RoutingTree + plural: RoutingTrees + schemas: + Matcher: + additionalProperties: false + properties: + label: + type: string + type: + enum: + - = + - '!=' + - =~ + - '!~' + type: string + value: + type: string + required: + - type + - label + - value + type: object + OperatorState: + additionalProperties: false + properties: + descriptiveState: + description: descriptiveState is an optional more descriptive state + field which has no requirements on format + type: string + details: + additionalProperties: + additionalProperties: {} + type: object + description: details contains any extra information that is operator-specific + type: object + lastEvaluation: + description: lastEvaluation is the ResourceVersion last evaluated + type: string + state: + description: |- + state describes the state of the lastEvaluation. + It is limited to three possible states for machine evaluation. + enum: + - success + - in_progress + - failed + type: string + required: + - lastEvaluation + - state + type: object + Route: + additionalProperties: false + properties: + active_time_intervals: + items: + type: string + type: array + continue: + type: boolean + group_by: + items: + type: string + type: array + group_interval: + type: string + group_wait: + type: string + matchers: + items: + $ref: '#/components/schemas/Matcher' + type: array + mute_time_intervals: + items: + type: string + type: array + receiver: + type: string + repeat_interval: + type: string + routes: + items: + $ref: '#/components/schemas/Route' + type: array + required: + - continue + type: object + RouteDefaults: + additionalProperties: false + properties: + group_by: + items: + type: string + type: array + group_interval: + type: string + group_wait: + type: string + receiver: + type: string + repeat_interval: + type: string + required: + - receiver + type: object + RoutingTree: + properties: + spec: + $ref: '#/components/schemas/spec' + status: + $ref: '#/components/schemas/status' + required: + - spec + spec: + additionalProperties: false + properties: + defaults: + $ref: '#/components/schemas/RouteDefaults' + routes: + items: + $ref: '#/components/schemas/Route' + type: array + required: + - defaults + - routes + type: object + status: + additionalProperties: false + properties: + additionalFields: + additionalProperties: + additionalProperties: {} + type: object + description: additionalFields is reserved for future use + type: object + operatorStates: + additionalProperties: + $ref: '#/components/schemas/OperatorState' + description: |- + operatorStates is a map of operator ID to operator state evaluations. + Any operator which consumes this kind SHOULD add its state evaluation information to this field. + type: object + type: object + scope: Namespaced + - conversion: false + kind: TemplateGroup + plural: TemplateGroups + schemas: + OperatorState: + additionalProperties: false + properties: + descriptiveState: + description: descriptiveState is an optional more descriptive state + field which has no requirements on format + type: string + details: + additionalProperties: + additionalProperties: {} + type: object + description: details contains any extra information that is operator-specific + type: object + lastEvaluation: + description: lastEvaluation is the ResourceVersion last evaluated + type: string + state: + description: |- + state describes the state of the lastEvaluation. + It is limited to three possible states for machine evaluation. + enum: + - success + - in_progress + - failed + type: string + required: + - lastEvaluation + - state + type: object + TemplateGroup: + properties: + spec: + $ref: '#/components/schemas/spec' + status: + $ref: '#/components/schemas/status' + required: + - spec + spec: + additionalProperties: false + properties: + content: + type: string + title: + type: string + required: + - title + - content + type: object + status: + additionalProperties: false + properties: + additionalFields: + additionalProperties: + additionalProperties: {} + type: object + description: additionalFields is reserved for future use + type: object + operatorStates: + additionalProperties: + $ref: '#/components/schemas/OperatorState' + description: |- + operatorStates is a map of operator ID to operator state evaluations. + Any operator which consumes this kind SHOULD add its state evaluation information to this field. + type: object + type: object + scope: Namespaced + - conversion: false + kind: TimeInterval + plural: TimeIntervals + schemas: + Interval: + additionalProperties: false + properties: + days_of_month: + items: + type: string + type: array + location: + type: string + months: + items: + type: string + type: array + times: + items: + $ref: '#/components/schemas/TimeRange' + type: array + weekdays: + items: + type: string + type: array + years: + items: + type: string + type: array + type: object + OperatorState: + additionalProperties: false + properties: + descriptiveState: + description: descriptiveState is an optional more descriptive state + field which has no requirements on format + type: string + details: + additionalProperties: + additionalProperties: {} + type: object + description: details contains any extra information that is operator-specific + type: object + lastEvaluation: + description: lastEvaluation is the ResourceVersion last evaluated + type: string + state: + description: |- + state describes the state of the lastEvaluation. + It is limited to three possible states for machine evaluation. + enum: + - success + - in_progress + - failed + type: string + required: + - lastEvaluation + - state + type: object + TimeInterval: + properties: + spec: + $ref: '#/components/schemas/spec' + status: + $ref: '#/components/schemas/status' + required: + - spec + TimeRange: + additionalProperties: false + properties: + end_time: + type: string + start_time: + type: string + required: + - start_time + - end_time + type: object + spec: + additionalProperties: false + properties: + name: + type: string + time_intervals: + items: + $ref: '#/components/schemas/Interval' + type: array + required: + - name + - time_intervals + type: object + status: + additionalProperties: false + properties: + additionalFields: + additionalProperties: + additionalProperties: {} + type: object + description: additionalFields is reserved for future use + type: object + operatorStates: + additionalProperties: + $ref: '#/components/schemas/OperatorState' + description: |- + operatorStates is a map of operator ID to operator state evaluations. + Any operator which consumes this kind SHOULD add its state evaluation information to this field. + type: object + type: object + scope: Namespaced + name: v0alpha1 + served: true diff --git a/apps/alerting/notifications/definitions/receiver.notifications.alerting.grafana.app.yaml b/apps/alerting/notifications/definitions/receiver.notifications.alerting.grafana.app.yaml new file mode 100644 index 00000000000..8fb7e899d41 --- /dev/null +++ b/apps/alerting/notifications/definitions/receiver.notifications.alerting.grafana.app.yaml @@ -0,0 +1,93 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: receivers.notifications.alerting.grafana.app +spec: + group: notifications.alerting.grafana.app + names: + kind: Receiver + plural: receivers + scope: Namespaced + versions: + - name: v0alpha1 + schema: + openAPIV3Schema: + properties: + spec: + properties: + integrations: + items: + properties: + disableResolveMessage: + type: boolean + secureFields: + additionalProperties: + type: boolean + type: object + settings: + type: object + x-kubernetes-preserve-unknown-fields: true + type: + type: string + uid: + type: string + version: + type: string + required: + - type + - version + - settings + type: object + type: array + title: + type: string + required: + - title + - integrations + 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. + It 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. + Any operator which consumes this kind SHOULD add its state evaluation information to this field. + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/apps/alerting/notifications/definitions/routingtree.notifications.alerting.grafana.app.yaml b/apps/alerting/notifications/definitions/routingtree.notifications.alerting.grafana.app.yaml new file mode 100644 index 00000000000..7071427cdd4 --- /dev/null +++ b/apps/alerting/notifications/definitions/routingtree.notifications.alerting.grafana.app.yaml @@ -0,0 +1,138 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: routingtrees.notifications.alerting.grafana.app +spec: + group: notifications.alerting.grafana.app + names: + kind: RoutingTree + plural: routingtrees + scope: Namespaced + versions: + - name: v0alpha1 + schema: + openAPIV3Schema: + properties: + spec: + properties: + defaults: + properties: + group_by: + items: + type: string + type: array + group_interval: + type: string + group_wait: + type: string + receiver: + type: string + repeat_interval: + type: string + required: + - receiver + type: object + routes: + items: + properties: + active_time_intervals: + items: + type: string + type: array + continue: + type: boolean + group_by: + items: + type: string + type: array + group_interval: + type: string + group_wait: + type: string + matchers: + items: + properties: + label: + type: string + type: + enum: + - = + - '!=' + - =~ + - '!~' + type: string + value: + type: string + required: + - type + - label + - value + type: object + type: array + mute_time_intervals: + items: + type: string + type: array + receiver: + type: string + repeat_interval: + type: string + routes: + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + required: + - continue + type: object + type: array + required: + - defaults + - routes + 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. + It 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. + Any operator which consumes this kind SHOULD add its state evaluation information to this field. + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/apps/alerting/notifications/definitions/templategroup.notifications.alerting.grafana.app.yaml b/apps/alerting/notifications/definitions/templategroup.notifications.alerting.grafana.app.yaml new file mode 100644 index 00000000000..6e28b1d22ad --- /dev/null +++ b/apps/alerting/notifications/definitions/templategroup.notifications.alerting.grafana.app.yaml @@ -0,0 +1,71 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: templategroups.notifications.alerting.grafana.app +spec: + group: notifications.alerting.grafana.app + names: + kind: TemplateGroup + plural: templategroups + scope: Namespaced + versions: + - name: v0alpha1 + schema: + openAPIV3Schema: + properties: + spec: + properties: + content: + type: string + title: + type: string + required: + - title + - content + 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. + It 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. + Any operator which consumes this kind SHOULD add its state evaluation information to this field. + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/apps/alerting/notifications/definitions/timeinterval.notifications.alerting.grafana.app.yaml b/apps/alerting/notifications/definitions/timeinterval.notifications.alerting.grafana.app.yaml new file mode 100644 index 00000000000..8d9c091681d --- /dev/null +++ b/apps/alerting/notifications/definitions/timeinterval.notifications.alerting.grafana.app.yaml @@ -0,0 +1,104 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: timeintervals.notifications.alerting.grafana.app +spec: + group: notifications.alerting.grafana.app + names: + kind: TimeInterval + plural: timeintervals + scope: Namespaced + versions: + - name: v0alpha1 + schema: + openAPIV3Schema: + properties: + spec: + properties: + name: + type: string + time_intervals: + items: + properties: + days_of_month: + items: + type: string + type: array + location: + type: string + months: + items: + type: string + type: array + times: + items: + properties: + end_time: + type: string + start_time: + type: string + required: + - start_time + - end_time + type: object + type: array + weekdays: + items: + type: string + type: array + years: + items: + type: string + type: array + type: object + type: array + required: + - name + - time_intervals + 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. + It 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. + Any operator which consumes this kind SHOULD add its state evaluation information to this field. + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/apps/alerting/notifications/kinds/manifest.cue b/apps/alerting/notifications/kinds/manifest.cue index 513bcb6d3b3..5ad62b79cd3 100644 --- a/apps/alerting/notifications/kinds/manifest.cue +++ b/apps/alerting/notifications/kinds/manifest.cue @@ -1,12 +1,20 @@ package kinds manifest: { - appName: "alerting" + appName: "alerting-notifications" groupOverride: "notifications.alerting.grafana.app" - kinds: [ - receiver, - routeTree, - templateGroup, - timeInterval, - ] + versions: { + "v0alpha1": { + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + kinds: [ + receiverv0alpha1, + routeTreev0alpha1, + templatev0alpha1, + timeIntervalv0alpha1, + ] + } + } } diff --git a/apps/alerting/notifications/kinds/receiver.cue b/apps/alerting/notifications/kinds/receiver.cue index bf0dba84987..719dd8ad699 100644 --- a/apps/alerting/notifications/kinds/receiver.cue +++ b/apps/alerting/notifications/kinds/receiver.cue @@ -4,25 +4,16 @@ import ( "github.com/grafana/grafana/apps/alerting/notifications/kinds/v0alpha1" ) -receiver: { +receiverKind: { kind: "Receiver" - apiResource: { - groupOverride: "notifications.alerting.grafana.app" - } pluralName: "Receivers" - current: "v0alpha1" - versions: { - "v0alpha1": { - codegen: { - ts: {enabled: false} - go: {enabled: true} - } - schema: { - spec: v0alpha1.ReceiverSpec - } - selectableFields: [ - "spec.title", - ] - } - } } + +receiverv0alpha1: receiverKind & { + schema: { + spec: v0alpha1.ReceiverSpec + } +// selectableFields: [ // TODO revisit when custom field selectors are supported +// "spec.title", +// ] +} \ No newline at end of file diff --git a/apps/alerting/notifications/kinds/routingtree.cue b/apps/alerting/notifications/kinds/routingtree.cue index 8fd527ff6f0..5b7bd06b54b 100644 --- a/apps/alerting/notifications/kinds/routingtree.cue +++ b/apps/alerting/notifications/kinds/routingtree.cue @@ -3,23 +3,13 @@ package kinds import ( "github.com/grafana/grafana/apps/alerting/notifications/kinds/v0alpha1" ) - -routeTree: { +routingTreeKind: { kind: "RoutingTree" - apiResource: { - groupOverride: "notifications.alerting.grafana.app" - } pluralName: "RoutingTrees" - current: "v0alpha1" - versions: { - "v0alpha1": { - codegen: { - ts: {enabled: false} - go: {enabled: true} - } - schema: { - spec: v0alpha1.RouteTreeSpec - } - } +} + +routeTreev0alpha1: routingTreeKind & { + schema: { + spec: v0alpha1.RouteTreeSpec } } diff --git a/apps/alerting/notifications/kinds/template.cue b/apps/alerting/notifications/kinds/template.cue index 9eae7fadf1d..03931a34a24 100644 --- a/apps/alerting/notifications/kinds/template.cue +++ b/apps/alerting/notifications/kinds/template.cue @@ -4,25 +4,16 @@ import ( "github.com/grafana/grafana/apps/alerting/notifications/kinds/v0alpha1" ) -templateGroup: { +templateKind: { kind: "TemplateGroup" - apiResource: { - groupOverride: "notifications.alerting.grafana.app" - } pluralName: "TemplateGroups" - current: "v0alpha1" - versions: { - "v0alpha1": { - codegen: { - ts: {enabled: false} - go: {enabled: true} - } - schema: { - spec: v0alpha1.TemplateGroupSpec - } - selectableFields: [ - "spec.title", - ] - } - } } + +templatev0alpha1: templateKind & { + schema: { + spec: v0alpha1.TemplateGroupSpec + } +// selectableFields: [ // TODO revisit when custom field selectors are supported +// "spec.title", +// ] +} \ No newline at end of file diff --git a/apps/alerting/notifications/kinds/timeInterval.cue b/apps/alerting/notifications/kinds/timeInterval.cue index 4b5004ff0e5..4ceaa3af370 100644 --- a/apps/alerting/notifications/kinds/timeInterval.cue +++ b/apps/alerting/notifications/kinds/timeInterval.cue @@ -3,26 +3,16 @@ package kinds import ( "github.com/grafana/grafana/apps/alerting/notifications/kinds/v0alpha1" ) - -timeInterval: { +timeIntervalKind: { kind: "TimeInterval" - apiResource: { - groupOverride: "notifications.alerting.grafana.app" - } pluralName: "TimeIntervals" - current: "v0alpha1" - versions: { - "v0alpha1": { - codegen: { - ts: {enabled: false} - go: {enabled: true} - } - schema: { - spec: v0alpha1.TimeIntervalSpec - } - selectableFields: [ - "spec.name", - ] - } - } } + +timeIntervalv0alpha1: timeIntervalKind & { + schema: { + spec: v0alpha1.TimeIntervalSpec + } +// selectableFields: [ // TODO revisit when custom field selectors are supported +// "spec.name", +// ] +} \ No newline at end of file diff --git a/apps/alerting/notifications/pkg/apis/alerting_manifest.go b/apps/alerting/notifications/pkg/apis/alerting_manifest.go deleted file mode 100644 index bde16bfb81d..00000000000 --- a/apps/alerting/notifications/pkg/apis/alerting_manifest.go +++ /dev/null @@ -1,100 +0,0 @@ -// -// This file is generated by grafana-app-sdk -// DO NOT EDIT -// - -package apis - -import ( - "fmt" - "strings" - - "github.com/grafana/grafana-app-sdk/app" - "github.com/grafana/grafana-app-sdk/resource" - - v0alpha1 "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" -) - -var appManifestData = app.ManifestData{ - AppName: "alerting", - Group: "notifications.alerting.grafana.app", - Versions: []app.ManifestVersion{ - { - Name: "v0alpha1", - Served: true, - Kinds: []app.ManifestVersionKind{ - { - Kind: "Receiver", - Plural: "Receivers", - Scope: "Namespaced", - Conversion: false, - SelectableFields: []string{ - "spec.title", - }, - }, - - { - Kind: "RoutingTree", - Plural: "RoutingTrees", - Scope: "Namespaced", - Conversion: false, - }, - - { - Kind: "TemplateGroup", - Plural: "TemplateGroups", - Scope: "Namespaced", - Conversion: false, - SelectableFields: []string{ - "spec.title", - }, - }, - - { - Kind: "TimeInterval", - Plural: "TimeIntervals", - Scope: "Namespaced", - Conversion: false, - SelectableFields: []string{ - "spec.name", - }, - }, - }, - }, - }, -} - -func LocalManifest() app.Manifest { - return app.NewEmbeddedManifest(appManifestData) -} - -func RemoteManifest() app.Manifest { - return app.NewAPIServerManifest("alerting") -} - -var kindVersionToGoType = map[string]resource.Kind{ - "Receiver/v0alpha1": v0alpha1.ReceiverKind(), - "RoutingTree/v0alpha1": v0alpha1.RoutingTreeKind(), - "TemplateGroup/v0alpha1": v0alpha1.TemplateGroupKind(), - "TimeInterval/v0alpha1": v0alpha1.TimeIntervalKind(), -} - -// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. -// If there is no association for the provided Kind and Version, exists will return false. -func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { - goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] - return goType, exists -} - -var customRouteToGoResponseType = map[string]any{} - -// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. -// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. -// If there is no association for the provided kind, version, custom route path, and method, exists will return false. -func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { - if len(path) > 0 && path[0] == '/' { - path = path[1:] - } - goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] - return goType, exists -} diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/constants.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/constants.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/constants.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/constants.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/ext.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/ext.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/ext.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/ext.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/fakes/timeinterval_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/fakes/timeinterval_gen.go similarity index 98% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/fakes/timeinterval_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/fakes/timeinterval_gen.go index 3cf2ffc8e7f..a17566b965a 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/fakes/timeinterval_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/fakes/timeinterval_gen.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" ) var UTCLocation = "UTC" diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_client_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_client_gen.go new file mode 100644 index 00000000000..057453e2257 --- /dev/null +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ReceiverClient struct { + client *resource.TypedClient[*Receiver, *ReceiverList] +} + +func NewReceiverClient(client resource.Client) *ReceiverClient { + return &ReceiverClient{ + client: resource.NewTypedClient[*Receiver, *ReceiverList](client, ReceiverKind()), + } +} + +func NewReceiverClientFromGenerator(generator resource.ClientGenerator) (*ReceiverClient, error) { + c, err := generator.ClientFor(ReceiverKind()) + if err != nil { + return nil, err + } + return NewReceiverClient(c), nil +} + +func (c *ReceiverClient) Get(ctx context.Context, identifier resource.Identifier) (*Receiver, error) { + return c.client.Get(ctx, identifier) +} + +func (c *ReceiverClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*ReceiverList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *ReceiverClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*ReceiverList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *ReceiverClient) Create(ctx context.Context, obj *Receiver, opts resource.CreateOptions) (*Receiver, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = ReceiverKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *ReceiverClient) Update(ctx context.Context, obj *Receiver, opts resource.UpdateOptions) (*Receiver, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *ReceiverClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Receiver, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *ReceiverClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus ReceiverStatus, opts resource.UpdateOptions) (*Receiver, error) { + return c.client.Update(ctx, &Receiver{ + TypeMeta: metav1.TypeMeta{ + Kind: ReceiverKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *ReceiverClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_codec_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_codec_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_codec_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_codec_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_ext.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_ext.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_ext.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_ext.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_metadata_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_metadata_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_metadata_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_metadata_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_object_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_object_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go similarity index 72% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_schema_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go index 314d36b602a..0987b4504bc 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_schema_gen.go @@ -5,25 +5,13 @@ package v0alpha1 import ( - "fmt" - "github.com/grafana/grafana-app-sdk/resource" ) // schema is unexported to prevent accidental overwrites var ( schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &Receiver{}, &ReceiverList{}, resource.WithKind("Receiver"), - resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ - FieldSelector: "spec.title", - FieldValueFunc: func(o resource.Object) (string, error) { - cast, ok := o.(*Receiver) - if !ok { - return "", fmt.Errorf("provided object must be of type *Receiver") - } - return cast.Spec.Title, nil - }, - }, - })) + resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope)) kindReceiver = resource.Kind{ Schema: schemaReceiver, Codecs: map[resource.KindEncoding]resource.Codec{ diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_spec_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_spec_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_spec_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_spec_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_status_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_status_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/receiver_status_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/receiver_status_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_client_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_client_gen.go new file mode 100644 index 00000000000..da611aa591a --- /dev/null +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type RoutingTreeClient struct { + client *resource.TypedClient[*RoutingTree, *RoutingTreeList] +} + +func NewRoutingTreeClient(client resource.Client) *RoutingTreeClient { + return &RoutingTreeClient{ + client: resource.NewTypedClient[*RoutingTree, *RoutingTreeList](client, RoutingTreeKind()), + } +} + +func NewRoutingTreeClientFromGenerator(generator resource.ClientGenerator) (*RoutingTreeClient, error) { + c, err := generator.ClientFor(RoutingTreeKind()) + if err != nil { + return nil, err + } + return NewRoutingTreeClient(c), nil +} + +func (c *RoutingTreeClient) Get(ctx context.Context, identifier resource.Identifier) (*RoutingTree, error) { + return c.client.Get(ctx, identifier) +} + +func (c *RoutingTreeClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*RoutingTreeList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *RoutingTreeClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*RoutingTreeList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *RoutingTreeClient) Create(ctx context.Context, obj *RoutingTree, opts resource.CreateOptions) (*RoutingTree, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = RoutingTreeKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *RoutingTreeClient) Update(ctx context.Context, obj *RoutingTree, opts resource.UpdateOptions) (*RoutingTree, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *RoutingTreeClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*RoutingTree, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *RoutingTreeClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus RoutingTreeStatus, opts resource.UpdateOptions) (*RoutingTree, error) { + return c.client.Update(ctx, &RoutingTree{ + TypeMeta: metav1.TypeMeta{ + Kind: RoutingTreeKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *RoutingTreeClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_codec_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_codec_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_codec_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_codec_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_ext.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_ext.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_ext.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_ext.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_metadata_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_metadata_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_metadata_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_metadata_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_object_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_object_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_schema_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_schema_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_schema_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_spec_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_spec_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_spec_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_spec_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_status_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_status_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/routingtree_status_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/routingtree_status_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_client_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_client_gen.go new file mode 100644 index 00000000000..54bbfb9ceb2 --- /dev/null +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type TemplateGroupClient struct { + client *resource.TypedClient[*TemplateGroup, *TemplateGroupList] +} + +func NewTemplateGroupClient(client resource.Client) *TemplateGroupClient { + return &TemplateGroupClient{ + client: resource.NewTypedClient[*TemplateGroup, *TemplateGroupList](client, TemplateGroupKind()), + } +} + +func NewTemplateGroupClientFromGenerator(generator resource.ClientGenerator) (*TemplateGroupClient, error) { + c, err := generator.ClientFor(TemplateGroupKind()) + if err != nil { + return nil, err + } + return NewTemplateGroupClient(c), nil +} + +func (c *TemplateGroupClient) Get(ctx context.Context, identifier resource.Identifier) (*TemplateGroup, error) { + return c.client.Get(ctx, identifier) +} + +func (c *TemplateGroupClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*TemplateGroupList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *TemplateGroupClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*TemplateGroupList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *TemplateGroupClient) Create(ctx context.Context, obj *TemplateGroup, opts resource.CreateOptions) (*TemplateGroup, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = TemplateGroupKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *TemplateGroupClient) Update(ctx context.Context, obj *TemplateGroup, opts resource.UpdateOptions) (*TemplateGroup, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *TemplateGroupClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*TemplateGroup, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *TemplateGroupClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus TemplateGroupStatus, opts resource.UpdateOptions) (*TemplateGroup, error) { + return c.client.Update(ctx, &TemplateGroup{ + TypeMeta: metav1.TypeMeta{ + Kind: TemplateGroupKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *TemplateGroupClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_codec_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_codec_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_codec_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_codec_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_ext.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_ext.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_ext.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_ext.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_metadata_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_metadata_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_metadata_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_metadata_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_object_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_object_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_schema_gen.go similarity index 73% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_schema_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_schema_gen.go index 509bfc4aeca..0be8cb1c6de 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_schema_gen.go @@ -5,25 +5,13 @@ package v0alpha1 import ( - "fmt" - "github.com/grafana/grafana-app-sdk/resource" ) // schema is unexported to prevent accidental overwrites var ( schemaTemplateGroup = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TemplateGroup{}, &TemplateGroupList{}, resource.WithKind("TemplateGroup"), - resource.WithPlural("templategroups"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ - FieldSelector: "spec.title", - FieldValueFunc: func(o resource.Object) (string, error) { - cast, ok := o.(*TemplateGroup) - if !ok { - return "", fmt.Errorf("provided object must be of type *TemplateGroup") - } - return cast.Spec.Title, nil - }, - }, - })) + resource.WithPlural("templategroups"), resource.WithScope(resource.NamespacedScope)) kindTemplateGroup = resource.Kind{ Schema: schemaTemplateGroup, Codecs: map[resource.KindEncoding]resource.Codec{ diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_spec_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_spec_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_spec_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_spec_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_status_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_status_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/templategroup_status_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/templategroup_status_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_client_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_client_gen.go new file mode 100644 index 00000000000..afa269ac96f --- /dev/null +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_client_gen.go @@ -0,0 +1,99 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type TimeIntervalClient struct { + client *resource.TypedClient[*TimeInterval, *TimeIntervalList] +} + +func NewTimeIntervalClient(client resource.Client) *TimeIntervalClient { + return &TimeIntervalClient{ + client: resource.NewTypedClient[*TimeInterval, *TimeIntervalList](client, TimeIntervalKind()), + } +} + +func NewTimeIntervalClientFromGenerator(generator resource.ClientGenerator) (*TimeIntervalClient, error) { + c, err := generator.ClientFor(TimeIntervalKind()) + if err != nil { + return nil, err + } + return NewTimeIntervalClient(c), nil +} + +func (c *TimeIntervalClient) Get(ctx context.Context, identifier resource.Identifier) (*TimeInterval, error) { + return c.client.Get(ctx, identifier) +} + +func (c *TimeIntervalClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*TimeIntervalList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *TimeIntervalClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*TimeIntervalList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *TimeIntervalClient) Create(ctx context.Context, obj *TimeInterval, opts resource.CreateOptions) (*TimeInterval, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = TimeIntervalKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *TimeIntervalClient) Update(ctx context.Context, obj *TimeInterval, opts resource.UpdateOptions) (*TimeInterval, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *TimeIntervalClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*TimeInterval, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *TimeIntervalClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus TimeIntervalStatus, opts resource.UpdateOptions) (*TimeInterval, error) { + return c.client.Update(ctx, &TimeInterval{ + TypeMeta: metav1.TypeMeta{ + Kind: TimeIntervalKind().Kind(), + APIVersion: GroupVersion.Identifier(), + }, + ObjectMeta: metav1.ObjectMeta{ + ResourceVersion: opts.ResourceVersion, + Namespace: identifier.Namespace, + Name: identifier.Name, + }, + Status: newStatus, + }, resource.UpdateOptions{ + Subresource: "status", + ResourceVersion: opts.ResourceVersion, + }) +} + +func (c *TimeIntervalClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_codec_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_codec_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_codec_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_codec_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_ext.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_ext.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_ext.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_ext.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_metadata_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_metadata_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_metadata_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_metadata_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_object_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_object_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_object_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_schema_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_schema_gen.go similarity index 73% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_schema_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_schema_gen.go index 8c5459f0059..715bfbc0fe7 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_schema_gen.go @@ -5,25 +5,13 @@ package v0alpha1 import ( - "fmt" - "github.com/grafana/grafana-app-sdk/resource" ) // schema is unexported to prevent accidental overwrites var ( schemaTimeInterval = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TimeInterval{}, &TimeIntervalList{}, resource.WithKind("TimeInterval"), - resource.WithPlural("timeintervals"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ - FieldSelector: "spec.name", - FieldValueFunc: func(o resource.Object) (string, error) { - cast, ok := o.(*TimeInterval) - if !ok { - return "", fmt.Errorf("provided object must be of type *TimeInterval") - } - return cast.Spec.Name, nil - }, - }, - })) + resource.WithPlural("timeintervals"), resource.WithScope(resource.NamespacedScope)) kindTimeInterval = resource.Kind{ Schema: schemaTimeInterval, Codecs: map[resource.KindEncoding]resource.Codec{ diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_spec_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_spec_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_spec_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_spec_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_status_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_status_gen.go similarity index 100% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/timeinterval_status_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/timeinterval_status_gen.go diff --git a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/zz_openapi_gen.go b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go similarity index 78% rename from apps/alerting/notifications/pkg/apis/alerting/v0alpha1/zz_openapi_gen.go rename to apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go index 5190f68f81d..782a0d1ca54 100644 --- a/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/zz_openapi_gen.go +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/zz_openapi_gen.go @@ -12,36 +12,36 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.Receiver": schema_pkg_apis_alerting_v0alpha1_Receiver(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.ReceiverIntegration": schema_pkg_apis_alerting_v0alpha1_ReceiverIntegration(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.ReceiverList": schema_pkg_apis_alerting_v0alpha1_ReceiverList(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.ReceiverSpec": schema_pkg_apis_alerting_v0alpha1_ReceiverSpec(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.ReceiverStatus": schema_pkg_apis_alerting_v0alpha1_ReceiverStatus(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.ReceiverstatusOperatorState": schema_pkg_apis_alerting_v0alpha1_ReceiverstatusOperatorState(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTree": schema_pkg_apis_alerting_v0alpha1_RoutingTree(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeList": schema_pkg_apis_alerting_v0alpha1_RoutingTreeList(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeMatcher": schema_pkg_apis_alerting_v0alpha1_RoutingTreeMatcher(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeRoute": schema_pkg_apis_alerting_v0alpha1_RoutingTreeRoute(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeRouteDefaults": schema_pkg_apis_alerting_v0alpha1_RoutingTreeRouteDefaults(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeSpec": schema_pkg_apis_alerting_v0alpha1_RoutingTreeSpec(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeStatus": schema_pkg_apis_alerting_v0alpha1_RoutingTreeStatus(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreestatusOperatorState": schema_pkg_apis_alerting_v0alpha1_RoutingTreestatusOperatorState(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TemplateGroup": schema_pkg_apis_alerting_v0alpha1_TemplateGroup(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TemplateGroupList": schema_pkg_apis_alerting_v0alpha1_TemplateGroupList(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TemplateGroupSpec": schema_pkg_apis_alerting_v0alpha1_TemplateGroupSpec(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TemplateGroupStatus": schema_pkg_apis_alerting_v0alpha1_TemplateGroupStatus(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TemplateGroupstatusOperatorState": schema_pkg_apis_alerting_v0alpha1_TemplateGroupstatusOperatorState(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeInterval": schema_pkg_apis_alerting_v0alpha1_TimeInterval(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalInterval": schema_pkg_apis_alerting_v0alpha1_TimeIntervalInterval(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalList": schema_pkg_apis_alerting_v0alpha1_TimeIntervalList(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalSpec": schema_pkg_apis_alerting_v0alpha1_TimeIntervalSpec(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalStatus": schema_pkg_apis_alerting_v0alpha1_TimeIntervalStatus(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalTimeRange": schema_pkg_apis_alerting_v0alpha1_TimeIntervalTimeRange(ref), - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalstatusOperatorState": schema_pkg_apis_alerting_v0alpha1_TimeIntervalstatusOperatorState(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.Receiver": schema_pkg_apis_alertingnotifications_v0alpha1_Receiver(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverIntegration": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverIntegration(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverList": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverList(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverSpec": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverSpec(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverStatus": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverStatus(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverstatusOperatorState": schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverstatusOperatorState(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTree": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTree(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeList": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeList(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeMatcher": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeMatcher(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRoute(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRouteDefaults": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRouteDefaults(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeSpec": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeSpec(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeStatus": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeStatus(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreestatusOperatorState": schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreestatusOperatorState(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroup": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroup(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupList": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupList(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupSpec": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupSpec(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupStatus": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupStatus(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupstatusOperatorState": schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupstatusOperatorState(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeInterval": schema_pkg_apis_alertingnotifications_v0alpha1_TimeInterval(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalInterval": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalInterval(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalList": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalList(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalSpec": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalSpec(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalStatus": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalStatus(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalTimeRange": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalTimeRange(ref), + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalstatusOperatorState": schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalstatusOperatorState(ref), } } -func schema_pkg_apis_alerting_v0alpha1_Receiver(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_Receiver(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -71,13 +71,13 @@ func schema_pkg_apis_alerting_v0alpha1_Receiver(ref common.ReferenceCallback) co SchemaProps: spec.SchemaProps{ Description: "Spec is the spec of the Receiver", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.ReceiverSpec"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverSpec"), }, }, "status": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.ReceiverStatus"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverStatus"), }, }, }, @@ -85,11 +85,11 @@ func schema_pkg_apis_alerting_v0alpha1_Receiver(ref common.ReferenceCallback) co }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.ReceiverSpec", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.ReceiverStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverSpec", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } -func schema_pkg_apis_alerting_v0alpha1_ReceiverIntegration(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverIntegration(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -157,7 +157,7 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverIntegration(ref common.ReferenceC } } -func schema_pkg_apis_alerting_v0alpha1_ReceiverList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -190,7 +190,7 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverList(ref common.ReferenceCallback Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.Receiver"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.Receiver"), }, }, }, @@ -201,11 +201,11 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverList(ref common.ReferenceCallback }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.Receiver", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.Receiver", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } -func schema_pkg_apis_alerting_v0alpha1_ReceiverSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -225,7 +225,7 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverSpec(ref common.ReferenceCallback Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.ReceiverIntegration"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverIntegration"), }, }, }, @@ -236,11 +236,11 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverSpec(ref common.ReferenceCallback }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.ReceiverIntegration"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverIntegration"}, } } -func schema_pkg_apis_alerting_v0alpha1_ReceiverStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -255,7 +255,7 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverStatus(ref common.ReferenceCallba Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.ReceiverstatusOperatorState"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverstatusOperatorState"), }, }, }, @@ -280,11 +280,11 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverStatus(ref common.ReferenceCallba }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.ReceiverstatusOperatorState"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.ReceiverstatusOperatorState"}, } } -func schema_pkg_apis_alerting_v0alpha1_ReceiverstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_ReceiverstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -335,7 +335,7 @@ func schema_pkg_apis_alerting_v0alpha1_ReceiverstatusOperatorState(ref common.Re } } -func schema_pkg_apis_alerting_v0alpha1_RoutingTree(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTree(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -365,13 +365,13 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTree(ref common.ReferenceCallback) SchemaProps: spec.SchemaProps{ Description: "Spec is the spec of the RoutingTree", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeSpec"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeSpec"), }, }, "status": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeStatus"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeStatus"), }, }, }, @@ -379,11 +379,11 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTree(ref common.ReferenceCallback) }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeSpec", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeSpec", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } -func schema_pkg_apis_alerting_v0alpha1_RoutingTreeList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -416,7 +416,7 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTreeList(ref common.ReferenceCallb Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTree"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTree"), }, }, }, @@ -427,11 +427,11 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTreeList(ref common.ReferenceCallb }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTree", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTree", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } -func schema_pkg_apis_alerting_v0alpha1_RoutingTreeMatcher(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeMatcher(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -465,7 +465,7 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTreeMatcher(ref common.ReferenceCa } } -func schema_pkg_apis_alerting_v0alpha1_RoutingTreeRoute(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRoute(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -484,7 +484,7 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTreeRoute(ref common.ReferenceCall Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeMatcher"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeMatcher"), }, }, }, @@ -546,7 +546,7 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTreeRoute(ref common.ReferenceCall Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeRoute"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute"), }, }, }, @@ -575,11 +575,11 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTreeRoute(ref common.ReferenceCall }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeMatcher", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeRoute"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeMatcher", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute"}, } } -func schema_pkg_apis_alerting_v0alpha1_RoutingTreeRouteDefaults(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeRouteDefaults(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -631,7 +631,7 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTreeRouteDefaults(ref common.Refer } } -func schema_pkg_apis_alerting_v0alpha1_RoutingTreeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -640,7 +640,7 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTreeSpec(ref common.ReferenceCallb "defaults": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeRouteDefaults"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRouteDefaults"), }, }, "routes": { @@ -650,7 +650,7 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTreeSpec(ref common.ReferenceCallb Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeRoute"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute"), }, }, }, @@ -661,11 +661,11 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTreeSpec(ref common.ReferenceCallb }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeRoute", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreeRouteDefaults"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRoute", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreeRouteDefaults"}, } } -func schema_pkg_apis_alerting_v0alpha1_RoutingTreeStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreeStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -680,7 +680,7 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTreeStatus(ref common.ReferenceCal Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreestatusOperatorState"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreestatusOperatorState"), }, }, }, @@ -705,11 +705,11 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTreeStatus(ref common.ReferenceCal }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.RoutingTreestatusOperatorState"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.RoutingTreestatusOperatorState"}, } } -func schema_pkg_apis_alerting_v0alpha1_RoutingTreestatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_RoutingTreestatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -760,7 +760,7 @@ func schema_pkg_apis_alerting_v0alpha1_RoutingTreestatusOperatorState(ref common } } -func schema_pkg_apis_alerting_v0alpha1_TemplateGroup(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroup(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -790,13 +790,13 @@ func schema_pkg_apis_alerting_v0alpha1_TemplateGroup(ref common.ReferenceCallbac SchemaProps: spec.SchemaProps{ Description: "Spec is the spec of the TemplateGroup", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TemplateGroupSpec"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupSpec"), }, }, "status": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TemplateGroupStatus"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupStatus"), }, }, }, @@ -804,11 +804,11 @@ func schema_pkg_apis_alerting_v0alpha1_TemplateGroup(ref common.ReferenceCallbac }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TemplateGroupSpec", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TemplateGroupStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupSpec", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } -func schema_pkg_apis_alerting_v0alpha1_TemplateGroupList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -841,7 +841,7 @@ func schema_pkg_apis_alerting_v0alpha1_TemplateGroupList(ref common.ReferenceCal Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TemplateGroup"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroup"), }, }, }, @@ -852,11 +852,11 @@ func schema_pkg_apis_alerting_v0alpha1_TemplateGroupList(ref common.ReferenceCal }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TemplateGroup", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroup", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } -func schema_pkg_apis_alerting_v0alpha1_TemplateGroupSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -883,7 +883,7 @@ func schema_pkg_apis_alerting_v0alpha1_TemplateGroupSpec(ref common.ReferenceCal } } -func schema_pkg_apis_alerting_v0alpha1_TemplateGroupStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -898,7 +898,7 @@ func schema_pkg_apis_alerting_v0alpha1_TemplateGroupStatus(ref common.ReferenceC Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TemplateGroupstatusOperatorState"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupstatusOperatorState"), }, }, }, @@ -923,11 +923,11 @@ func schema_pkg_apis_alerting_v0alpha1_TemplateGroupStatus(ref common.ReferenceC }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TemplateGroupstatusOperatorState"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TemplateGroupstatusOperatorState"}, } } -func schema_pkg_apis_alerting_v0alpha1_TemplateGroupstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_TemplateGroupstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -978,7 +978,7 @@ func schema_pkg_apis_alerting_v0alpha1_TemplateGroupstatusOperatorState(ref comm } } -func schema_pkg_apis_alerting_v0alpha1_TimeInterval(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeInterval(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1008,13 +1008,13 @@ func schema_pkg_apis_alerting_v0alpha1_TimeInterval(ref common.ReferenceCallback SchemaProps: spec.SchemaProps{ Description: "Spec is the spec of the TimeInterval", Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalSpec"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalSpec"), }, }, "status": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalStatus"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalStatus"), }, }, }, @@ -1022,11 +1022,11 @@ func schema_pkg_apis_alerting_v0alpha1_TimeInterval(ref common.ReferenceCallback }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalSpec", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalSpec", "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } -func schema_pkg_apis_alerting_v0alpha1_TimeIntervalInterval(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalInterval(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1039,7 +1039,7 @@ func schema_pkg_apis_alerting_v0alpha1_TimeIntervalInterval(ref common.Reference Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalTimeRange"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalTimeRange"), }, }, }, @@ -1111,11 +1111,11 @@ func schema_pkg_apis_alerting_v0alpha1_TimeIntervalInterval(ref common.Reference }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalTimeRange"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalTimeRange"}, } } -func schema_pkg_apis_alerting_v0alpha1_TimeIntervalList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1148,7 +1148,7 @@ func schema_pkg_apis_alerting_v0alpha1_TimeIntervalList(ref common.ReferenceCall Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeInterval"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeInterval"), }, }, }, @@ -1159,11 +1159,11 @@ func schema_pkg_apis_alerting_v0alpha1_TimeIntervalList(ref common.ReferenceCall }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeInterval", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeInterval", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } -func schema_pkg_apis_alerting_v0alpha1_TimeIntervalSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1183,7 +1183,7 @@ func schema_pkg_apis_alerting_v0alpha1_TimeIntervalSpec(ref common.ReferenceCall Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalInterval"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalInterval"), }, }, }, @@ -1194,11 +1194,11 @@ func schema_pkg_apis_alerting_v0alpha1_TimeIntervalSpec(ref common.ReferenceCall }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalInterval"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalInterval"}, } } -func schema_pkg_apis_alerting_v0alpha1_TimeIntervalStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1213,7 +1213,7 @@ func schema_pkg_apis_alerting_v0alpha1_TimeIntervalStatus(ref common.ReferenceCa Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalstatusOperatorState"), + Ref: ref("github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalstatusOperatorState"), }, }, }, @@ -1238,11 +1238,11 @@ func schema_pkg_apis_alerting_v0alpha1_TimeIntervalStatus(ref common.ReferenceCa }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1.TimeIntervalstatusOperatorState"}, + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1.TimeIntervalstatusOperatorState"}, } } -func schema_pkg_apis_alerting_v0alpha1_TimeIntervalTimeRange(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalTimeRange(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1269,7 +1269,7 @@ func schema_pkg_apis_alerting_v0alpha1_TimeIntervalTimeRange(ref common.Referenc } } -func schema_pkg_apis_alerting_v0alpha1_TimeIntervalstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_alertingnotifications_v0alpha1_TimeIntervalstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ diff --git a/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go new file mode 100644 index 00000000000..3537a6e3e8d --- /dev/null +++ b/apps/alerting/notifications/pkg/apis/alertingnotifications_manifest.go @@ -0,0 +1,157 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package apis + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kube-openapi/pkg/spec3" + + v0alpha1 "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" +) + +var ( + rawSchemaReceiverv0alpha1 = []byte(`{"Integration":{"additionalProperties":false,"properties":{"disableResolveMessage":{"type":"boolean"},"secureFields":{"additionalProperties":{"type":"boolean"},"type":"object"},"settings":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"type":{"type":"string"},"uid":{"type":"string"},"version":{"type":"string"}},"required":["type","version","settings"],"type":"object"},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Receiver":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"integrations":{"items":{"$ref":"#/components/schemas/Integration"},"type":"array"},"title":{"type":"string"}},"required":["title","integrations"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + versionSchemaReceiverv0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaReceiverv0alpha1, &versionSchemaReceiverv0alpha1) + rawSchemaRoutingTreev0alpha1 = []byte(`{"Matcher":{"additionalProperties":false,"properties":{"label":{"type":"string"},"type":{"enum":["=","!=","=~","!~"],"type":"string"},"value":{"type":"string"}},"required":["type","label","value"],"type":"object"},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Route":{"additionalProperties":false,"properties":{"active_time_intervals":{"items":{"type":"string"},"type":"array"},"continue":{"type":"boolean"},"group_by":{"items":{"type":"string"},"type":"array"},"group_interval":{"type":"string"},"group_wait":{"type":"string"},"matchers":{"items":{"$ref":"#/components/schemas/Matcher"},"type":"array"},"mute_time_intervals":{"items":{"type":"string"},"type":"array"},"receiver":{"type":"string"},"repeat_interval":{"type":"string"},"routes":{"items":{"$ref":"#/components/schemas/Route"},"type":"array"}},"required":["continue"],"type":"object"},"RouteDefaults":{"additionalProperties":false,"properties":{"group_by":{"items":{"type":"string"},"type":"array"},"group_interval":{"type":"string"},"group_wait":{"type":"string"},"receiver":{"type":"string"},"repeat_interval":{"type":"string"}},"required":["receiver"],"type":"object"},"RoutingTree":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"defaults":{"$ref":"#/components/schemas/RouteDefaults"},"routes":{"items":{"$ref":"#/components/schemas/Route"},"type":"array"}},"required":["defaults","routes"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + versionSchemaRoutingTreev0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaRoutingTreev0alpha1, &versionSchemaRoutingTreev0alpha1) + rawSchemaTemplateGroupv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"TemplateGroup":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"content":{"type":"string"},"title":{"type":"string"}},"required":["title","content"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + versionSchemaTemplateGroupv0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaTemplateGroupv0alpha1, &versionSchemaTemplateGroupv0alpha1) + rawSchemaTimeIntervalv0alpha1 = []byte(`{"Interval":{"additionalProperties":false,"properties":{"days_of_month":{"items":{"type":"string"},"type":"array"},"location":{"type":"string"},"months":{"items":{"type":"string"},"type":"array"},"times":{"items":{"$ref":"#/components/schemas/TimeRange"},"type":"array"},"weekdays":{"items":{"type":"string"},"type":"array"},"years":{"items":{"type":"string"},"type":"array"}},"type":"object"},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"TimeInterval":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"TimeRange":{"additionalProperties":false,"properties":{"end_time":{"type":"string"},"start_time":{"type":"string"}},"required":["start_time","end_time"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"name":{"type":"string"},"time_intervals":{"items":{"$ref":"#/components/schemas/Interval"},"type":"array"}},"required":["name","time_intervals"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + versionSchemaTimeIntervalv0alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaTimeIntervalv0alpha1, &versionSchemaTimeIntervalv0alpha1) +) + +var appManifestData = app.ManifestData{ + AppName: "alerting-notifications", + Group: "notifications.alerting.grafana.app", + Versions: []app.ManifestVersion{ + { + Name: "v0alpha1", + Served: true, + Kinds: []app.ManifestVersionKind{ + { + Kind: "Receiver", + Plural: "Receivers", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaReceiverv0alpha1, + }, + + { + Kind: "RoutingTree", + Plural: "RoutingTrees", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaRoutingTreev0alpha1, + }, + + { + Kind: "TemplateGroup", + Plural: "TemplateGroups", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaTemplateGroupv0alpha1, + }, + + { + Kind: "TimeInterval", + Plural: "TimeIntervals", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaTimeIntervalv0alpha1, + }, + }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + Cluster: map[string]spec3.PathProps{}, + }, + }, + }, +} + +func LocalManifest() app.Manifest { + return app.NewEmbeddedManifest(appManifestData) +} + +func RemoteManifest() app.Manifest { + return app.NewAPIServerManifest("alerting-notifications") +} + +var kindVersionToGoType = map[string]resource.Kind{ + "Receiver/v0alpha1": v0alpha1.ReceiverKind(), + "RoutingTree/v0alpha1": v0alpha1.RoutingTreeKind(), + "TemplateGroup/v0alpha1": v0alpha1.TemplateGroupKind(), + "TimeInterval/v0alpha1": v0alpha1.TimeIntervalKind(), +} + +// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. +// If there is no association for the provided Kind and Version, exists will return false. +func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { + goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] + return goType, exists +} + +var customRouteToGoResponseType = map[string]any{} + +// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. +// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. +// If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) +func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoParamsType = map[string]runtime.Object{} + +func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +type GoTypeAssociator struct{} + +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + +func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { + return ManifestGoTypeAssociator(kind, version) +} +func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteResponsesAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { + return ManifestCustomRouteQueryAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/alerting/notifications/pkg/apis/type.go b/apps/alerting/notifications/pkg/apis/type.go index 3c15f5284b5..51b9103ca45 100644 --- a/apps/alerting/notifications/pkg/apis/type.go +++ b/apps/alerting/notifications/pkg/apis/type.go @@ -4,7 +4,7 @@ import ( sdkResource "github.com/grafana/grafana-app-sdk/resource" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" ) func GetKinds() map[schema.GroupVersion][]sdkResource.Kind { diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 7e99af69887..c0fc9ae8bde 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -200,7 +200,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/google/wire v0.6.0 // indirect + github.com/google/wire v0.7.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.2 // indirect github.com/gorilla/mux v1.8.1 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 2f6549ac9c1..5b212cbc647 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -694,12 +694,11 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= -github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= +github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= +github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -1333,7 +1332,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= @@ -1469,12 +1467,9 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1518,10 +1513,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1578,11 +1569,6 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1621,10 +1607,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1717,24 +1699,15 @@ golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1746,10 +1719,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1818,10 +1787,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= diff --git a/devenv/create_docker_compose.sh b/devenv/create_docker_compose.sh index d937f95cec4..baa50818f11 100755 --- a/devenv/create_docker_compose.sh +++ b/devenv/create_docker_compose.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +shopt -s nullglob # Enable nullglob + blocks_dir=docker/blocks docker_dir=docker template_dir=templates @@ -8,6 +10,8 @@ grafana_config_file=conf.tmp grafana_config=config compose_header_file=docker/compose_header.yml +compose_volume_section_file=docker/compose_volume_section.yml +compose_volume_section_create_flag=docker_volume_create_true compose_file=docker-compose.yaml env_file=.env @@ -60,3 +64,27 @@ for dir in $@; do fi done +volume_files=$($blocks_dir/**/$compose_volume_section_create_flag) + +if [[ ${#volume_files[@]} -ne 0 ]]; then + echo "Adding volume section to $compose_file" + cat $compose_volume_section_file >> $compose_file + echo "" >> $compose_file + + for dir in $@; do + current_dir=$blocks_dir/$dir + if [ ! -d "$current_dir" ]; then + echo "$current_dir is not a directory" + exit 1 + fi + + + if [ -f $current_dir/$compose_volume_section_create_flag ]; then + echo "Adding volume for $current_dir to $compose_file" + echo " $dir-data-volume:" >> $compose_file + echo "" >> $compose_file + fi + done + + cat $compose_file +fi diff --git a/devenv/docker/compose_volume_section.yml b/devenv/docker/compose_volume_section.yml new file mode 100644 index 00000000000..f61bd276b2c --- /dev/null +++ b/devenv/docker/compose_volume_section.yml @@ -0,0 +1 @@ +volumes: \ No newline at end of file diff --git a/docs/sources/upgrade-guide/upgrade-v12.2/index.md b/docs/sources/upgrade-guide/upgrade-v12.2/index.md new file mode 100644 index 00000000000..d2605fc51d1 --- /dev/null +++ b/docs/sources/upgrade-guide/upgrade-v12.2/index.md @@ -0,0 +1,20 @@ +--- +description: Guide for upgrading to Grafana v12.2 +keywords: + - grafana + - configuration + - documentation + - upgrade + - '12.2' +title: Upgrade to Grafana v12.2 +menuTitle: Upgrade to v12.2 +weight: 498 +--- + +# Upgrade to Grafana v12.2 + +{{< docs/shared lookup="upgrade/intro_2.md" source="grafana" version="" >}} + +{{< docs/shared lookup="back-up/back-up-grafana.md" source="grafana" version="" leveloffset="+1" >}} + +{{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} diff --git a/docs/sources/whatsnew/_index.md b/docs/sources/whatsnew/_index.md index fb478253a93..bb401ea136d 100644 --- a/docs/sources/whatsnew/_index.md +++ b/docs/sources/whatsnew/_index.md @@ -192,6 +192,7 @@ For a complete list of every change, with links to pull requests and related iss ## Grafana 12 +- [What's new in 12.2](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v12-2) - [What's new in 12.1](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v12-1) - [What's new in 12.0](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v12-0) diff --git a/docs/sources/whatsnew/whats-new-in-v12-2.md b/docs/sources/whatsnew/whats-new-in-v12-2.md new file mode 100644 index 00000000000..bc8314d56be --- /dev/null +++ b/docs/sources/whatsnew/whats-new-in-v12-2.md @@ -0,0 +1,72 @@ +--- +description: Feature and improvement highlights for Grafana v12.2 +keywords: + - grafana + - new + - documentation + - '12.2' + - release notes +labels: + products: + - cloud + - enterprise + - oss +title: What's new in Grafana v12.2 +posts: + - title: SQL expressions + items: + - whats-new/2025-09-05-sql-expressions.md + - title: Dashboards and visualizations + items: + - whats-new/2025-08-22-new-table-visualization-is-generally-available.md + - whats-new/2025-08-27-generate-tooltips-from-table-fields.md + - whats-new/2025-08-27-improved-footer-for-table-visualization.md + - whats-new/2025-07-17-disable-tooltips-in-canvas-visualizations.md + - whats-new/2025-07-14-static-options-for-query-variable.md + - whats-new/2025-07-24-dynamic-connection-direction-in-canvas.md + - whats-new/2025-08-04-canvas-pan-zoom-improvements.md + - whats-new/2025-09-01-actions-authentication-via-infinity-datasource.md + - whats-new/2025-09-02-enhanced-ad-hoc-filter-support.md + - whats-new/2025-09-02-new-dashboard-apis-now-enabled-by-default.md + - title: Reporting + items: + - whats-new/2025-05-27-new-and-improved-reporting.md + - title: Data sources + items: + - whats-new/2025-08-12-jenkins-enterprise-data-source-for-grafana.md + - whats-new/2025-07-16-google-sheets-data-source-now-supports-template-variables.md + - whats-new/2025-09-04-azure-monitor-resource-picker-filtering-and-recent-resources.md + - title: Explore + items: + - whats-new/2025-07-08-saved-queries-in-dashboards-and-explore.md + - title: Logs Drilldown + items: + - whats-new/2025-08-29-json-log-line-viewer-in-logs-drilldown-is-now-generally-available.md + - title: Metrics Drilldown + items: + - whats-new/2025-08-07-grafana-metrics-drilldown-entry-point-from-alerting-rule.md + - title: Plugins + items: + - whats-new/2025-09-11-translate-your-plugin.md + - title: Authentication and authorization + items: + - whats-new/2025-09-10-scim-configuration-ui.md +whats_new_grafana_version: 12.2 +weight: -51 +--- + +# What’s new in Grafana v12.2 + +Welcome to Grafana 12.2! This release focuses on making it easier to gain insights from your data. + +We're excited to announce several features are now GA. Enhanced ad hoc filtering transforms your dashboards into true command centers, allowing you to slice and dice datasets on the fly. The redesigned table visualization offers improved performance and visual aids for quick pattern and anomaly identification, helping you make faster decisions. The Logs Drilldown JSON viewer makes intimidating log structures organized and explorable. Metrics Drilldown now integrates with alert creation in Grafana, so you can explore Prometheus data with intuitive point-and-click interactions, find the right visualization, and easily use its query in your alert rule. + +We're also collecting feedback on some new public preview features. AI-powered SQL expressions eliminate the barrier between questions and answers by generating SQL queries from natural language and providing instant explanations for existing queries. Our enhanced Canvas Pan and Zoom experience lets you design complex dashboards exactly as you envision them. + +Keep reading to learn more about everything 12.2 has in store. + +{{< youtube id=-7A_tePidEM >}} + +For even more detail about all the changes in this release, refer to the [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). For the specific steps we recommend when you upgrade to v12.2, check out our [Upgrade Guide](https://grafana.com/docs/grafana//upgrade-guide/upgrade-v12.2/). + +{{< docs/whats-new >}} diff --git a/e2e-playwright/dashboard-cujs/dashboard-view.spec.ts b/e2e-playwright/dashboard-cujs/dashboard-view.spec.ts index 767ec2424e6..9fccafada83 100644 --- a/e2e-playwright/dashboard-cujs/dashboard-view.spec.ts +++ b/e2e-playwright/dashboard-cujs/dashboard-view.spec.ts @@ -134,8 +134,8 @@ test.describe( expect.soft(await timePickerButton.textContent()).toContain('2024-01-01 08:30:00 to 2024-01-01 08:40:00'); - const forwardBtn = page.locator('button[aria-label="Move time range forwards"]'); - const backwardBtn = page.locator('button[aria-label="Move time range backwards"]'); + const forwardBtn = page.locator('button[aria-label*="Move"][aria-label*="forward"]'); + const backwardBtn = page.locator('button[aria-label*="Move"][aria-label*="backward"]'); await forwardBtn.click(); diff --git a/e2e-playwright/dashboard-cujs/scope-cujs.spec.ts b/e2e-playwright/dashboard-cujs/scope-cujs.spec.ts index 25f4a9233d5..919930027b8 100644 --- a/e2e-playwright/dashboard-cujs/scope-cujs.spec.ts +++ b/e2e-playwright/dashboard-cujs/scope-cujs.spec.ts @@ -169,12 +169,15 @@ test.describe( await searchScopes(page, scopeSearchOne, [secondLevelScopes[0]]); await expect.soft(scopeTreeCheckboxes).toHaveCount(1); - expect.soft(await scopeTreeCheckboxes.first().locator('../..').textContent()).toBe(scopeSearchOne); + + // Check grandparent title, to make sure we get what we want + expect.soft(await scopeTreeCheckboxes.first().locator('../../..').textContent()).toBe(scopeSearchOne); await searchScopes(page, scopeSearchTwo, [secondLevelScopes[1]]); await expect.soft(scopeTreeCheckboxes).toHaveCount(1); - expect.soft(await scopeTreeCheckboxes.first().locator('../..').textContent()).toBe(scopeSearchTwo); + // Check grandparent title, to make sure we get what we want + expect.soft(await scopeTreeCheckboxes.first().locator('../../..').textContent()).toBe(scopeSearchTwo); }); }); } diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts index 852fe00fa85..11c20cd9702 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts @@ -3,13 +3,14 @@ import { test, expect } from '@grafana/plugin-e2e'; import pluginJson from '../plugin.json'; import testApp3pluginJson from '../plugins/grafana-extensionexample3-app/plugin.json'; import { testIds } from '../testIds'; +import { selectors } from '@grafana/e2e-selectors'; test.describe('grafana-extensionstest-app', { tag: ['@plugins'] }, () => { test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { await page.goto(`/a/${pluginJson.id}/added-links`); const section = await page.getByTestId(testIds.addedLinksPage.section1); await section.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Go to A').click(); + await page.getByTestId(selectors.components.Portal.container).getByText('Go to A').click(); await page.getByTestId(testIds.modal.open).click(); await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); }); @@ -18,7 +19,7 @@ test.describe('grafana-extensionstest-app', { tag: ['@plugins'] }, () => { await page.goto(`/a/${pluginJson.id}/added-links`); const section = await page.getByTestId(testIds.addedLinksPage.section1); await section.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Open from B').click(); + await page.getByTestId(selectors.components.Portal.container).getByText('Open from B').click(); await expect(page.getByTestId(testIds.appB.modal)).toBeVisible(); }); @@ -26,7 +27,7 @@ test.describe('grafana-extensionstest-app', { tag: ['@plugins'] }, () => { await page.goto(`/a/${pluginJson.id}/added-links`); const section = await page.getByTestId(testIds.addedLinksPage.section1); await section.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Basic link').click(); + await page.getByTestId(selectors.components.Portal.container).getByText('Basic link').click(); await page.getByTestId(testIds.modal.open).click(); await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); }); diff --git a/e2e-playwright/utils/scope-helpers.ts b/e2e-playwright/utils/scope-helpers.ts index 2c27697f85d..300350e4c47 100644 --- a/e2e-playwright/utils/scope-helpers.ts +++ b/e2e-playwright/utils/scope-helpers.ts @@ -262,13 +262,15 @@ export async function getScopeLeafName(page: Page, nth: number): Promise } export async function getScopeLeafTitle(page: Page, nth: number): Promise { - const locator = page.getByTestId(/^scopes-tree-.*-(checkbox|radio)/).nth(nth); - const scopeTitle = await locator.locator('../..').textContent(); - + const leafLocator = page.getByTestId(/^scopes-tree-.*-(checkbox|radio)/).nth(nth); + // Find the closest ancestor element that has the main tree item test id + const titleLocator = leafLocator.locator( + 'xpath=ancestor::*[@data-testid][starts-with(@data-testid, "scopes-tree-") and not(contains(@data-testid, "-checkbox")) and not(contains(@data-testid, "-radio")) and not(contains(@data-testid, "-expand"))]' + ); + const scopeTitle = await titleLocator.textContent(); if (!scopeTitle) { throw new Error('There are no scopes in the selector'); } - return scopeTitle; } diff --git a/e2e-playwright/various-suite/keybinds.spec.ts b/e2e-playwright/various-suite/keybinds.spec.ts index 4ff37889838..74549a620e8 100644 --- a/e2e-playwright/various-suite/keybinds.spec.ts +++ b/e2e-playwright/various-suite/keybinds.spec.ts @@ -85,5 +85,63 @@ test.describe( expectedRange = 'Time range selected: 2024-06-05 10:04:00 to 2024-06-05 10:05:00'; // 1 min back await expect(timePickerButton).toHaveAttribute('aria-label', expectedRange); }); + + test('ctrl+o should toggle shared crosshair', async ({ page, selectors }) => { + // Navigate to a new dashboard + await page.goto('/dashboard/new?orgId=1'); + + // Wait for dashboard to load + await page.waitForLoadState('networkidle'); + + // Wait for dashboard to be fully initialized by checking for dashboard content + await page + .locator('[data-testid*="dashboard"]') + .or(page.locator('text=Start your new dashboard')) + .first() + .waitFor({ state: 'visible' }); + + // Test the keyboard shortcut first in the main dashboard view + const currentUrl = page.url(); + const modKey = process.platform === 'darwin' ? 'Meta' : 'Control'; + + // Test that mod+o works in the main dashboard (should not trigger file dialog) + console.log('Testing mod+o in main dashboard view...'); + await page.keyboard.press(`${modKey}+o`); + expect(page.url()).toBe(currentUrl); // Should not navigate away + + // Now open settings to check if the state actually changed + await page.keyboard.press('d'); + await page.keyboard.press('s'); + + // Wait for settings page to load by checking for the General tab or settings content + await page + .locator('text=General') + .or(page.locator('[data-testid*="dashboard-settings"]')) + .waitFor({ state: 'visible' }); + + // Wait for Panel options section to be visible and scroll to it + const panelOptionsSection = page.locator('text=Panel options'); + await panelOptionsSection.waitFor({ state: 'visible' }); + await panelOptionsSection.scrollIntoViewIfNeeded(); + + // Wait for radio buttons to be visible + await page + .locator('[role="radiogroup"]') + .last() + .locator('input[type="radio"]') + .first() + .waitFor({ state: 'visible' }); + + // Check current state - after one mod+o press, it should be crosshair (1) + await expect(page.locator('[role="radiogroup"]').last().locator('input[type="radio"]').nth(1)).toBeChecked(); // Shared crosshair + + // Test second press in the main dashboard view (should go to tooltip) + await page.keyboard.press(`${modKey}+o`); + await expect(page.locator('[role="radiogroup"]').last().locator('input[type="radio"]').nth(2)).toBeChecked(); // Shared tooltip + + // Test third press in the main dashboard view (should go back to default) + await page.keyboard.press(`${modKey}+o`); + await expect(page.locator('[role="radiogroup"]').last().locator('input[type="radio"]').nth(0)).toBeChecked(); // Default + }); } ); diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 00e5d96765e..0786a4e35a6 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1809,11 +1809,6 @@ "count": 1 } }, - "public/app/features/alerting/unified/rule-list/StateView.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "public/app/features/alerting/unified/types/alerting.ts": { "@typescript-eslint/no-explicit-any": { "count": 5 diff --git a/go.mod b/go.mod index 125078c7571..15102b1c9ca 100644 --- a/go.mod +++ b/go.mod @@ -82,7 +82,7 @@ require ( github.com/google/go-github/v70 v70.0.0 // indirect; @grafana/grafana-git-ui-sync-team github.com/google/go-querystring v1.1.0 // indirect; @grafana/oss-big-tent github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group - github.com/google/wire v0.6.0 // @grafana/grafana-backend-group + github.com/google/wire v0.7.0 // @grafana/grafana-backend-group github.com/googleapis/gax-go/v2 v2.14.2 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad diff --git a/go.sum b/go.sum index ace71828555..3fb8164e1ca 100644 --- a/go.sum +++ b/go.sum @@ -1528,7 +1528,6 @@ github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkj github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -1538,8 +1537,8 @@ github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= -github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= +github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= +github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= @@ -2785,7 +2784,6 @@ golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= @@ -3204,7 +3202,6 @@ golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= diff --git a/go.work.sum b/go.work.sum index 5d029982567..918fc645382 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1055,6 +1055,7 @@ github.com/grafana/dskit v0.0.0-20250818234656-8ff9c6532e85/go.mod h1:kImsvJ1xnm github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak= github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90= github.com/grafana/gomemcache v0.0.0-20250228145437-da7b95fd2ac1/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= +github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM= github.com/grafana/grafana-app-sdk v0.41.0 h1:SYHN3U7B1myRKY3UZZDkFsue9TDmAOap0UrQVTqtYBU= github.com/grafana/grafana-app-sdk v0.41.0/go.mod h1:Wg/3vEZfok1hhIWiHaaJm+FwkosfO98o8KbeLFEnZpY= @@ -1068,8 +1069,6 @@ github.com/grafana/grafana-app-sdk/logging v0.43.1/go.mod h1:0xrjKSGY5z+NLGuGsXQ github.com/grafana/grafana-app-sdk/logging v0.43.2/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-app-sdk/plugin v0.41.0 h1:ShUvGpAVzM3UxcsfwS6l/lwW4ytDeTbCQXf8w2P8Yp8= github.com/grafana/grafana-app-sdk/plugin v0.41.0/go.mod h1:YIhimVfAqtOp3kdhxOanaSZjypVKh/bYxf9wfFfhDm0= -github.com/grafana/grafana-app-sdk/plugin v0.45.0 h1:rK4FL5h7SqGBDeUdrUjHVGnwgN6w8deQAKx8gJ27Iew= -github.com/grafana/grafana-app-sdk/plugin v0.45.0/go.mod h1:fZ6lWVMWr0EpkmyocxZ7MTTc9x6b2jYM93+gjlMgVD4= github.com/grafana/grafana-aws-sdk v0.38.2 h1:TzQD0OpWsNjtldi5G5TLDlBRk8OyDf+B5ujcoAu4Dp0= github.com/grafana/grafana-aws-sdk v0.38.2/go.mod h1:j3vi+cXYHEFqjhBGrI6/lw1TNM+dl0Y3f0cSnDOPy+s= github.com/grafana/grafana-aws-sdk v1.0.2 h1:98eBuHYFmgvH0xO9kKf4RBsEsgQRp8EOA/9yhDIpkss= @@ -2034,6 +2033,7 @@ golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.24.1/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= diff --git a/package.json b/package.json index b5a74906856..3bece98c2ab 100644 --- a/package.json +++ b/package.json @@ -286,8 +286,8 @@ "@grafana/plugin-ui": "^0.10.10", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "6.35.3", - "@grafana/scenes-react": "6.35.3", + "@grafana/scenes": "6.38.0", + "@grafana/scenes-react": "6.38.0", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/packages/grafana-alerting/src/grafana/api/v0alpha1/api.gen.ts b/packages/grafana-alerting/src/grafana/api/v0alpha1/api.gen.ts index 6166032bb5d..f4e16d6a4c1 100644 --- a/packages/grafana-alerting/src/grafana/api/v0alpha1/api.gen.ts +++ b/packages/grafana-alerting/src/grafana/api/v0alpha1/api.gen.ts @@ -1249,6 +1249,7 @@ export type ReceiverIntegration = { }; type: string; uid?: string; + version: string; }; export type ReceiverSpec = { integrations: ReceiverIntegration[]; diff --git a/packages/grafana-alerting/src/grafana/api/v0alpha1/mocks/fakes/Receivers.ts b/packages/grafana-alerting/src/grafana/api/v0alpha1/mocks/fakes/Receivers.ts index f2f5563d25f..aa432591ba2 100644 --- a/packages/grafana-alerting/src/grafana/api/v0alpha1/mocks/fakes/Receivers.ts +++ b/packages/grafana-alerting/src/grafana/api/v0alpha1/mocks/fakes/Receivers.ts @@ -47,10 +47,12 @@ export const GenericIntegrationFactory = Factory.define(() => ({ settings: { foo: 'bar', }, + version: 'v1', // Add version field })); export const EmailIntegrationFactory = Factory.define(() => ({ type: 'email', + version: 'v1', settings: { addresses: faker.internet.email(), }, @@ -58,6 +60,7 @@ export const EmailIntegrationFactory = Factory.define(() => ({ export const SlackIntegrationFactory = Factory.define(() => ({ type: 'slack', + version: 'v1', settings: { mentionChannel: '#alerts', }, @@ -70,5 +73,6 @@ export const ContactPointMetadataAnnotationsFactory = Factory.define; diff --git a/packages/grafana-data/src/themes/palette.ts b/packages/grafana-data/src/themes/palette.ts index 5934e144710..feba8a1252e 100644 --- a/packages/grafana-data/src/themes/palette.ts +++ b/packages/grafana-data/src/themes/palette.ts @@ -16,7 +16,7 @@ export const palette = { darkBorder2: '#64646b', // Dashboard bg / layer 0 (light theme) - gray90: '#f4f5f5', + gray90: '#fbfbfb', // Card bg / layer 1 gray100: '#f4f5f5', // divider line diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e00a616e91c..12bd9dcfb86 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -111,6 +111,10 @@ export interface FeatureToggles { */ influxdbBackendMigration?: boolean; /** + * populate star status from apiserver + */ + starsFromAPIServer?: boolean; + /** * Enable streaming JSON parser for InfluxDB datasource InfluxQL query language */ influxqlStreamingParser?: boolean; diff --git a/packages/grafana-ui/src/components/Button/Button.tsx b/packages/grafana-ui/src/components/Button/Button.tsx index 83b1c5825c0..c596904a39b 100644 --- a/packages/grafana-ui/src/components/Button/Button.tsx +++ b/packages/grafana-ui/src/components/Button/Button.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { GrafanaTheme2, ThemeRichColor } from '@grafana/data'; import { useTheme2 } from '../../themes/ThemeContext'; -import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins'; +import { getButtonFocusStyles, getMouseFocusStyles } from '../../themes/mixins'; import { IconName, IconSize, IconType } from '../../types/icon'; import { ComponentSize } from '../../types/size'; import { getPropertiesForButtonSize } from '../Forms/commonStyles'; @@ -234,7 +234,7 @@ export const getButtonStyles = (props: StyleProps) => { const { height, padding, fontSize } = getPropertiesForButtonSize(size, theme); const variantStyles = getPropertiesForVariant(theme, variant, fill); const disabledStyles = getPropertiesForDisabled(theme, variant, fill); - const focusStyle = getFocusStyles(theme); + const focusStyle = getButtonFocusStyles(theme); const paddingMinusBorder = theme.spacing.gridSize * padding - 1; return { @@ -263,6 +263,12 @@ export const getButtonStyles = (props: StyleProps) => { ...variantStyles, ':disabled': disabledStyles, '&[disabled]': disabledStyles, + + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: theme.transitions.create(['background-color', 'border-color', 'color'], { + duration: theme.transitions.duration.short, + }), + }, }), disabled: css(disabledStyles, { '&:hover': css(disabledStyles), @@ -290,12 +296,18 @@ export const getButtonStyles = (props: StyleProps) => { }; }; -function getButtonVariantStyles(theme: GrafanaTheme2, color: ThemeRichColor, fill: ButtonFill) { +export function getActiveButtonStyles(color: ThemeRichColor, fill: ButtonFill) { + return { + background: fill === 'solid' ? color.main : 'transparent', + }; +} + +export function getButtonVariantStyles(theme: GrafanaTheme2, color: ThemeRichColor, fill: ButtonFill) { let outlineBorderColor = color.border; let borderColor = 'transparent'; let hoverBorderColor = 'transparent'; - // Secondary button has some special rules as we lack theem color token to + // Secondary button has some special rules as we lack the color token to // specify border color for normal button vs border color for outline button if (color.name === 'secondary') { borderColor = color.border; @@ -308,15 +320,16 @@ function getButtonVariantStyles(theme: GrafanaTheme2, color: ThemeRichColor, fil background: 'transparent', color: color.text, border: `1px solid ${outlineBorderColor}`, - transition: theme.transitions.create(['background-color', 'border-color', 'color'], { - duration: theme.transitions.duration.short, - }), - '&:hover': { + '&:hover, &:focus': { background: color.transparent, borderColor: theme.colors.emphasize(outlineBorderColor, 0.25), color: color.text, }, + + '&:active': { + ...getActiveButtonStyles(color, fill), + }, }; } @@ -325,18 +338,15 @@ function getButtonVariantStyles(theme: GrafanaTheme2, color: ThemeRichColor, fil background: 'transparent', color: color.text, border: '1px solid transparent', - transition: theme.transitions.create(['background-color', 'color'], { - duration: theme.transitions.duration.short, - }), - '&:focus': { - outline: 'none', - textDecoration: 'none', - }, - - '&:hover': { + '&:hover, &:focus': { background: color.transparent, textDecoration: 'none', + outline: 'none', + }, + + '&:active': { + ...getActiveButtonStyles(color, fill), }, }; } @@ -345,9 +355,6 @@ function getButtonVariantStyles(theme: GrafanaTheme2, color: ThemeRichColor, fil background: color.main, color: color.contrastText, border: `1px solid ${borderColor}`, - transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { - duration: theme.transitions.duration.short, - }), '&:hover': { background: color.shade, @@ -355,6 +362,15 @@ function getButtonVariantStyles(theme: GrafanaTheme2, color: ThemeRichColor, fil boxShadow: theme.shadows.z1, borderColor: hoverBorderColor, }, + + '&:focus': { + background: color.shade, + color: color.contrastText, + }, + + '&:active': { + ...getActiveButtonStyles(color, fill), + }, }; } @@ -364,6 +380,7 @@ function getPropertiesForDisabled(theme: GrafanaTheme2, variant: ButtonVariant, boxShadow: 'none', color: theme.colors.text.disabled, transition: 'none', + background: theme.colors.action.disabledBackground, }; if (fill === 'text') { diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx index 926fcce5615..3157b6ca837 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx @@ -55,6 +55,8 @@ export interface TimeRangePickerProps { onChangeFiscalYearStartMonth?: (month: number) => void; onMoveBackward: () => void; onMoveForward: () => void; + moveForwardTooltip?: string; + moveBackwardTooltip?: string; onZoom: () => void; onError?: (error?: string) => void; history?: TimeRange[]; @@ -78,6 +80,8 @@ export function TimeRangePicker(props: TimeRangePickerProps) { value, onMoveBackward, onMoveForward, + moveForwardTooltip, + moveBackwardTooltip, onZoom, onError, timeZone, @@ -150,11 +154,14 @@ export function TimeRangePicker(props: TimeRangePickerProps) { return ( @@ -207,12 +214,15 @@ export function TimeRangePicker(props: TimeRangePickerProps) { {timeSyncButton} diff --git a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx index 199aa1c279e..12e3eb08354 100644 --- a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx +++ b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx @@ -1,17 +1,15 @@ -import { css } from '@emotion/css'; -import { autoUpdate, offset, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react'; -import { FocusScope } from '@react-aria/focus'; import { memo, HTMLAttributes, useState } from 'react'; -import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { SelectableValue } from '@grafana/data'; -import { useStyles2 } from '../../themes/ThemeContext'; -import { getPositioningMiddleware } from '../../utils/floating'; import { Menu } from '../Menu/Menu'; import { MenuItem } from '../Menu/MenuItem'; +import { ScrollContainer } from '../ScrollContainer/ScrollContainer'; import { ToolbarButton, ToolbarButtonVariant } from '../ToolbarButton/ToolbarButton'; import { PopoverContent } from '../Tooltip/types'; +import { Dropdown } from './Dropdown'; + export interface Props extends HTMLAttributes { className?: string; options: Array>; @@ -29,70 +27,34 @@ export interface Props extends HTMLAttributes { */ const ButtonSelectComponent = (props: Props) => { const { className, options, value, onChange, narrow, variant, ...restProps } = props; - const styles = useStyles2(getStyles); const [isOpen, setIsOpen] = useState(false); - const placement = 'bottom-end'; - // the order of middleware is important! - const middleware = [offset(0), ...getPositioningMiddleware(placement)]; - - const { context, refs, floatingStyles } = useFloating({ - open: isOpen, - placement, - onOpenChange: setIsOpen, - middleware, - whileElementsMounted: autoUpdate, - }); - - const click = useClick(context); - const dismiss = useDismiss(context); - - const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]); - - const onChangeInternal = (item: SelectableValue) => { - onChange(item); - setIsOpen(false); - }; + const renderMenu = () => ( + setIsOpen(false)}> + + {options.map((item) => ( + onChange(item)} + active={item.value === value?.value} + ariaChecked={item.value === value?.value} + ariaLabel={item.ariaLabel || item.label} + disabled={item.isDisabled} + component={item.component} + role="menuitemradio" + /> + ))} + + + ); return ( -
- + + {value?.label || (value?.value != null ? String(value?.value) : null)} - {isOpen && ( -
- - {/* - tabIndex=-1 is needed here to support highlighting text within the menu when using FocusScope - see https://github.com/adobe/react-spectrum/issues/1604#issuecomment-781574668 - */} - setIsOpen(false)}> - {options.map((item) => ( - onChangeInternal(item)} - active={item.value === value?.value} - ariaChecked={item.value === value?.value} - ariaLabel={item.ariaLabel || item.label} - disabled={item.isDisabled} - component={item.component} - role="menuitemradio" - /> - ))} - - -
- )} -
+ ); }; @@ -102,17 +64,3 @@ ButtonSelectComponent.displayName = 'ButtonSelect'; // see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/37087#issuecomment-656596623 // eslint-disable-next-line @typescript-eslint/consistent-type-assertions export const ButtonSelect = memo(ButtonSelectComponent) as typeof ButtonSelectComponent; - -const getStyles = (theme: GrafanaTheme2) => { - return { - wrapper: css({ - position: 'relative', - display: 'inline-flex', - }), - menuWrapper: css({ - zIndex: theme.zIndex.dropdown, - maxHeight: '100vh', - overflow: 'auto', - }), - }; -}; diff --git a/packages/grafana-ui/src/components/IconButton/IconButton.tsx b/packages/grafana-ui/src/components/IconButton/IconButton.tsx index 2964143b331..bbc4519f71f 100644 --- a/packages/grafana-ui/src/components/IconButton/IconButton.tsx +++ b/packages/grafana-ui/src/components/IconButton/IconButton.tsx @@ -1,13 +1,13 @@ import { css, cx } from '@emotion/css'; import * as React from 'react'; -import { GrafanaTheme2, colorManipulator, deprecationWarning } from '@grafana/data'; +import { GrafanaTheme2, deprecationWarning } from '@grafana/data'; import { useStyles2 } from '../../themes/ThemeContext'; import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins'; import { IconName, IconSize, IconType } from '../../types/icon'; import { ComponentSize } from '../../types/size'; -import { IconRenderer } from '../Button/Button'; +import { getActiveButtonStyles, IconRenderer } from '../Button/Button'; import { getSvgSize } from '../Icon/utils'; import { Tooltip } from '../Tooltip/Tooltip'; import { PopoverContent, TooltipPlacement } from '../Tooltip/types'; @@ -107,13 +107,17 @@ const getStyles = (theme: GrafanaTheme2, size: IconSize, variant: IconButtonVari // overall size of the IconButton on hover // theme.spacing.gridSize originates from 2*4px for padding and letting the IconSize generally decide on the hoverSize const hoverSize = getSvgSize(size) + theme.spacing.gridSize; + const activeButtonStyle = getActiveButtonStyles(theme.colors.secondary, 'text'); - let iconColor = theme.colors.text.primary; + let iconColor = theme.colors.primary.text; + let hoverColor = theme.colors.primary.transparent; - if (variant === 'primary') { - iconColor = theme.colors.primary.text; + if (variant === 'secondary') { + iconColor = theme.colors.secondary.text; + hoverColor = theme.colors.secondary.transparent; } else if (variant === 'destructive') { iconColor = theme.colors.error.text; + hoverColor = theme.colors.error.transparent; } return { @@ -129,11 +133,21 @@ const getStyles = (theme: GrafanaTheme2, size: IconSize, variant: IconButtonVari alignItems: 'center', padding: 0, color: iconColor, + borderRadius: theme.shape.radius.default, + + '&:active': { + '&:before, &:hover:before': { + backgroundColor: activeButtonStyle.background, + }, + }, '&[disabled], &:disabled': { cursor: 'not-allowed', color: theme.colors.action.disabledText, opacity: 0.65, + '&:hover:before': { + backgroundColor: 'transparent', + }, }, '&:before': { @@ -155,12 +169,9 @@ const getStyles = (theme: GrafanaTheme2, size: IconSize, variant: IconButtonVari '&:focus:not(:focus-visible)': getMouseFocusStyles(theme), - '&:hover': { - '&:before': { - backgroundColor: - variant === 'secondary' ? theme.colors.action.hover : colorManipulator.alpha(iconColor, 0.12), - opacity: 1, - }, + '&:hover:before': { + backgroundColor: hoverColor, + opacity: 1, }, }), icon: css({ diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx index a866ccefc9e..376b1b3fc38 100644 --- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx +++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx @@ -8,7 +8,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { useStyles2 } from '../../themes/ThemeContext'; import { getFocusStyles, getMouseFocusStyles, mediaUp } from '../../themes/mixins'; import { IconSize } from '../../types/icon'; -import { getPropertiesForVariant } from '../Button/Button'; +import { getActiveButtonStyles, getPropertiesForVariant } from '../Button/Button'; import { Icon } from '../Icon/Icon'; import { Tooltip } from '../Tooltip/Tooltip'; @@ -134,11 +134,15 @@ const getStyles = (theme: GrafanaTheme2) => { color: theme.colors.text.primary, background: theme.colors.secondary.main, - '&:hover': { + '&:hover, &:focus': { color: theme.colors.text.primary, background: theme.colors.secondary.shade, border: `1px solid ${theme.colors.border.medium}`, }, + + '&:active': { + ...getActiveButtonStyles(theme.colors.secondary, 'solid'), + }, }); return { @@ -155,7 +159,7 @@ const getStyles = (theme: GrafanaTheme2) => { border: `1px solid ${theme.colors.secondary.border}`, whiteSpace: 'nowrap', [theme.transitions.handleMotion('no-preference', 'reduce')]: { - transition: theme.transitions.create(['background', 'box-shadow', 'border-color', 'color'], { + transition: theme.transitions.create(['background-color', 'border-color', 'color'], { duration: theme.transitions.duration.short, }), }, @@ -189,10 +193,14 @@ const getStyles = (theme: GrafanaTheme2) => { background: 'transparent', border: `1px solid transparent`, - '&:hover': { + '&:hover, &:focus': { color: theme.colors.text.primary, background: theme.colors.action.hover, }, + + '&:active': { + ...getActiveButtonStyles(theme.colors.secondary, 'solid'), + }, }), canvas: defaultOld, active: cx( diff --git a/packages/grafana-ui/src/themes/mixins.ts b/packages/grafana-ui/src/themes/mixins.ts index 5127047ada0..39bb7b19c83 100644 --- a/packages/grafana-ui/src/themes/mixins.ts +++ b/packages/grafana-ui/src/themes/mixins.ts @@ -72,6 +72,13 @@ export function getFocusStyles(theme: GrafanaTheme2) { }; } +export function getButtonFocusStyles(theme: GrafanaTheme2) { + return { + ...getFocusStyles(theme), + transitionProperty: undefined, + }; +} + // max-width is set up based on .grafana-tooltip class that's used in dashboard export const getTooltipContainerStyles = (theme: GrafanaTheme2) => ({ overflow: 'hidden', diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 2416a2ff2a2..aee2e869399 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -60,7 +60,7 @@ func (hs *HTTPServer) isDashboardStarredByUser(c *contextmodel.ReqContext, dashU return false, err } - query := star.IsStarredByUserQuery{UserID: userID, DashboardUID: dashUID} + query := star.IsStarredByUserQuery{UserID: userID, OrgID: c.OrgID, DashboardUID: dashUID} return hs.starService.IsStarredByUser(c.Req.Context(), &query) } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index cdf85c8594d..7c47656e208 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -31,7 +31,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "github.com/grafana/grafana/pkg/services/annotations/annotationstest" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" @@ -129,7 +128,6 @@ func newTestLive(t *testing.T, store db.DB) *live.GrafanaLive { nil, features, acimpl.ProvideAccessControl(features), &dashboards.FakeDashboardService{}, - annotationstest.NewFakeAnnotationsRepo(), nil, nil) require.NoError(t, err) return gLive diff --git a/pkg/apiserver/storage/testing/watcher_tests.go b/pkg/apiserver/storage/testing/watcher_tests.go index 371a3d612f9..89ea37d4b95 100644 --- a/pkg/apiserver/storage/testing/watcher_tests.go +++ b/pkg/apiserver/storage/testing/watcher_tests.go @@ -684,7 +684,7 @@ func RunTestClusterScopedWatch(ctx context.Context, t *testing.T, store storage. currentObjs := map[string]*example.Pod{} for _, watchTest := range tt.watchTests { out := &example.Pod{} - key := "pods/ns-1/" + watchTest.obj.Name + key := "pods/" + watchTest.obj.Name err := store.GuaranteedUpdate(ctx, key, out, true, nil, storage.SimpleUpdate( func(runtime.Object) (runtime.Object, error) { obj := watchTest.obj.DeepCopy() @@ -1607,15 +1607,15 @@ func clusterScopedNodeNameAttrFunc(obj runtime.Object) (labels.Set, fields.Set, } func basePod(podName string) *example.Pod { - return baseNamespacedPod(podName, "ns-1") + return baseNamespacedPod(podName, "") } func basePodUpdated(podName string) *example.Pod { - return baseNamespacedPodUpdated(podName, "ns-1") + return baseNamespacedPodUpdated(podName, "") } func basePodAssigned(podName, nodeName string) *example.Pod { - return baseNamespacedPodAssigned(podName, "ns-1", nodeName) + return baseNamespacedPodAssigned(podName, "", nodeName) } func baseNamespacedPod(podName, namespace string) *example.Pod { diff --git a/pkg/operators/provisioning/config.go b/pkg/operators/provisioning/config.go index 98d1fb78d5e..273070ac156 100644 --- a/pkg/operators/provisioning/config.go +++ b/pkg/operators/provisioning/config.go @@ -69,7 +69,7 @@ type provisioningControllerConfig struct { // local_permitted_prefixes = // [provisioning] // repository_types = -func setupFromConfig(cfg *setting.Cfg) (controllerCfg *provisioningControllerConfig, err error) { +func setupFromConfig(cfg *setting.Cfg, registry prometheus.Registerer) (controllerCfg *provisioningControllerConfig, err error) { if cfg == nil { return nil, fmt.Errorf("no configuration available") } @@ -130,7 +130,7 @@ func setupFromConfig(cfg *setting.Cfg) (controllerCfg *provisioningControllerCon return nil, fmt.Errorf("failed to setup decrypter: %w", err) } - repoFactory, err := setupRepoFactory(cfg, decrypter, provisioningClient) + repoFactory, err := setupRepoFactory(cfg, decrypter, provisioningClient, registry) if err != nil { return nil, fmt.Errorf("failed to setup repository getter: %w", err) } @@ -220,6 +220,7 @@ func setupRepoFactory( cfg *setting.Cfg, decrypter repository.Decrypter, provisioningClient *client.Clientset, + registry prometheus.Registerer, ) (repository.Factory, error) { operatorSec := cfg.SectionWithEnvOverrides("operator") provisioningSec := cfg.SectionWithEnvOverrides("provisioning") @@ -246,7 +247,7 @@ func setupRepoFactory( var webhook *webhooks.WebhookExtraBuilder provisioningAppURL := operatorSec.Key("provisioning_server_public_url").String() if provisioningAppURL != "" { - webhook = webhooks.ProvideWebhooks(provisioningAppURL) + webhook = webhooks.ProvideWebhooks(provisioningAppURL, registry) } extras = append(extras, github.Extra( diff --git a/pkg/operators/provisioning/jobs_operator.go b/pkg/operators/provisioning/jobs_operator.go index 1da6600c724..f901988e0e0 100644 --- a/pkg/operators/provisioning/jobs_operator.go +++ b/pkg/operators/provisioning/jobs_operator.go @@ -10,8 +10,10 @@ import ( "time" "github.com/grafana/grafana-app-sdk/logging" + "github.com/prometheus/client_golang/prometheus" "k8s.io/client-go/tools/cache" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/export" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs/migrate" @@ -33,7 +35,17 @@ func RunJobController(deps server.OperatorDependencies) error { })).With("logger", "provisioning-job-controller") logger.Info("Starting provisioning job controller") - controllerCfg, err := setupJobsControllerFromConfig(deps.Config) + tracingConfig, err := tracing.ProvideTracingConfig(deps.Config) + if err != nil { + return fmt.Errorf("failed to provide tracing config: %w", err) + } + + tracer, err := tracing.ProvideService(tracingConfig) + if err != nil { + return fmt.Errorf("failed to provide tracing service: %w", err) + } + + controllerCfg, err := setupJobsControllerFromConfig(deps.Config, deps.Registerer) if err != nil { return fmt.Errorf("failed to setup operator: %w", err) } @@ -94,12 +106,12 @@ func RunJobController(deps server.OperatorDependencies) error { // } jobHistoryWriter := jobs.NewAPIClientHistoryWriter(controllerCfg.provisioningClient.ProvisioningV0alpha1()) - jobStore, err := jobs.NewJobStore(controllerCfg.provisioningClient.ProvisioningV0alpha1(), 30*time.Second) + jobStore, err := jobs.NewJobStore(controllerCfg.provisioningClient.ProvisioningV0alpha1(), 30*time.Second, deps.Registerer) if err != nil { return fmt.Errorf("create API client job store: %w", err) } - workers, err := setupWorkers(controllerCfg) + workers, err := setupWorkers(controllerCfg, deps.Registerer, tracer) if err != nil { return fmt.Errorf("setup workers: %w", err) } @@ -111,15 +123,16 @@ func RunJobController(deps server.OperatorDependencies) error { // This is basically our own JobQueue system driver, err := jobs.NewConcurrentJobDriver( - 3, // 3 drivers for now - 20*time.Minute, // Max time for each job - time.Minute, // Cleanup jobs - 30*time.Second, // Periodically look for new jobs - 30*time.Second, // Lease renewal interval + controllerCfg.concurrentDrivers, + controllerCfg.maxJobTimeout, + controllerCfg.cleanupInterval, + controllerCfg.jobInterval, + controllerCfg.leaseRenewalInterval, jobStore, repoGetter, jobHistoryWriter, jobController.InsertNotifications(), + deps.Registerer, workers..., ) if err != nil { @@ -148,11 +161,16 @@ func RunJobController(deps server.OperatorDependencies) error { type jobsControllerConfig struct { provisioningControllerConfig - historyExpiration time.Duration + historyExpiration time.Duration + maxJobTimeout time.Duration + cleanupInterval time.Duration + jobInterval time.Duration + leaseRenewalInterval time.Duration + concurrentDrivers int } -func setupJobsControllerFromConfig(cfg *setting.Cfg) (*jobsControllerConfig, error) { - controllerCfg, err := setupFromConfig(cfg) +func setupJobsControllerFromConfig(cfg *setting.Cfg, registry prometheus.Registerer) (*jobsControllerConfig, error) { + controllerCfg, err := setupFromConfig(cfg, registry) if err != nil { return nil, err } @@ -160,10 +178,15 @@ func setupJobsControllerFromConfig(cfg *setting.Cfg) (*jobsControllerConfig, err return &jobsControllerConfig{ provisioningControllerConfig: *controllerCfg, historyExpiration: cfg.SectionWithEnvOverrides("operator").Key("history_expiration").MustDuration(0), + concurrentDrivers: cfg.SectionWithEnvOverrides("operator").Key("concurrent_drivers").MustInt(3), + maxJobTimeout: cfg.SectionWithEnvOverrides("operator").Key("max_job_timeout").MustDuration(20 * time.Minute), + cleanupInterval: cfg.SectionWithEnvOverrides("operator").Key("cleanup_interval").MustDuration(time.Minute), + jobInterval: cfg.SectionWithEnvOverrides("operator").Key("job_interval").MustDuration(30 * time.Second), + leaseRenewalInterval: cfg.SectionWithEnvOverrides("operator").Key("lease_renewal_interval").MustDuration(30 * time.Second), }, nil } -func setupWorkers(controllerCfg *jobsControllerConfig) ([]jobs.Worker, error) { +func setupWorkers(controllerCfg *jobsControllerConfig, registry prometheus.Registerer, tracer tracing.Tracer) ([]jobs.Worker, error) { clients := controllerCfg.clients parsers := resources.NewParserFactory(clients) resourceLister := resources.NewResourceLister(controllerCfg.unified) @@ -172,14 +195,18 @@ func setupWorkers(controllerCfg *jobsControllerConfig) ([]jobs.Worker, error) { workers := make([]jobs.Worker, 0) + metrics := jobs.RegisterJobMetrics(registry) + // Sync - syncer := sync.NewSyncer(sync.Compare, sync.FullSync, sync.IncrementalSync) + syncer := sync.NewSyncer(sync.Compare, sync.FullSync, sync.IncrementalSync, tracer) syncWorker := sync.NewSyncWorker( clients, repositoryResources, nil, // HACK: we have updated the worker to check for nil statusPatcher.Patch, syncer, + metrics, + tracer, ) workers = append(workers, syncWorker) @@ -190,6 +217,7 @@ func setupWorkers(controllerCfg *jobsControllerConfig) ([]jobs.Worker, error) { repositoryResources, export.ExportAll, stageIfPossible, + metrics, ) workers = append(workers, exportWorker) @@ -204,11 +232,11 @@ func setupWorkers(controllerCfg *jobsControllerConfig) ([]jobs.Worker, error) { workers = append(workers, migrationWorker) // Delete - deleteWorker := deletepkg.NewWorker(syncWorker, stageIfPossible, repositoryResources) + deleteWorker := deletepkg.NewWorker(syncWorker, stageIfPossible, repositoryResources, metrics) workers = append(workers, deleteWorker) // Move - moveWorker := move.NewWorker(syncWorker, stageIfPossible, repositoryResources) + moveWorker := move.NewWorker(syncWorker, stageIfPossible, repositoryResources, metrics) workers = append(workers, moveWorker) return workers, nil diff --git a/pkg/operators/provisioning/repo_operator.go b/pkg/operators/provisioning/repo_operator.go index 6162007f8e4..0ec243bb41e 100644 --- a/pkg/operators/provisioning/repo_operator.go +++ b/pkg/operators/provisioning/repo_operator.go @@ -11,8 +11,10 @@ import ( "github.com/grafana/grafana-app-sdk/logging" appcontroller "github.com/grafana/grafana/apps/provisioning/pkg/controller" + "github.com/prometheus/client_golang/prometheus" "k8s.io/client-go/tools/cache" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" @@ -28,11 +30,21 @@ func RunRepoController(deps server.OperatorDependencies) error { })).With("logger", "provisioning-repo-controller") logger.Info("Starting provisioning repo controller") - controllerCfg, err := getRepoControllerConfig(deps.Config) + controllerCfg, err := getRepoControllerConfig(deps.Config, deps.Registerer) if err != nil { return fmt.Errorf("failed to setup operator: %w", err) } + tracingConfig, err := tracing.ProvideTracingConfig(deps.Config) + if err != nil { + return fmt.Errorf("failed to provide tracing config: %w", err) + } + + tracer, err := tracing.ProvideService(tracingConfig) + if err != nil { + return fmt.Errorf("failed to provide tracing service: %w", err) + } + ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -50,12 +62,12 @@ func RunRepoController(deps server.OperatorDependencies) error { ) resourceLister := resources.NewResourceLister(controllerCfg.unified) - jobs, err := jobs.NewJobStore(controllerCfg.provisioningClient.ProvisioningV0alpha1(), 30*time.Second) + jobs, err := jobs.NewJobStore(controllerCfg.provisioningClient.ProvisioningV0alpha1(), 30*time.Second, deps.Registerer) if err != nil { return fmt.Errorf("create API client job store: %w", err) } statusPatcher := appcontroller.NewRepositoryStatusPatcher(controllerCfg.provisioningClient.ProvisioningV0alpha1()) - healthChecker := controller.NewHealthChecker(statusPatcher) + healthChecker := controller.NewHealthChecker(statusPatcher, deps.Registerer) repoInformer := informerFactory.Provisioning().V0alpha1().Repositories() controller, err := controller.NewRepositoryController( @@ -68,6 +80,8 @@ func RunRepoController(deps server.OperatorDependencies) error { nil, // dualwrite -- standalone operator assumes it is backed by unified storage healthChecker, statusPatcher, + deps.Registerer, + tracer, ) if err != nil { return fmt.Errorf("failed to create repository controller: %w", err) @@ -87,8 +101,8 @@ type repoControllerConfig struct { workerCount int } -func getRepoControllerConfig(cfg *setting.Cfg) (*repoControllerConfig, error) { - controllerCfg, err := setupFromConfig(cfg) +func getRepoControllerConfig(cfg *setting.Cfg, registry prometheus.Registerer) (*repoControllerConfig, error) { + controllerCfg, err := setupFromConfig(cfg, registry) if err != nil { return nil, err } diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 31c2be5ea89..cfec57b7f41 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -675,17 +675,21 @@ func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer { } func (b *DashboardsAPIBuilder) verifyFolderAccessPermissions(ctx context.Context, user identity.Requester, folderIds ...string) error { - scopes := []string{} for _, folderId := range folderIds { - scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderId)) - } - ok, err := b.accessControl.Evaluate(ctx, user, accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, scopes...)) - if err != nil { - return err - } + resp, err := b.folderClient.Get(ctx, folderId, user.GetOrgID(), metav1.GetOptions{}, "access") + if err != nil { + return dashboards.ErrFolderAccessDenied + } + var accessInfo folders.FolderAccessInfo + err = runtime.DefaultUnstructuredConverter.FromUnstructured(resp.Object, &accessInfo) + if err != nil { + b.log.Error("Failed to convert folder access response", "error", err) + return dashboards.ErrFolderAccessDenied + } - if !ok { - return dashboards.ErrFolderAccessDenied + if !accessInfo.CanEdit { + return dashboards.ErrFolderAccessDenied + } } return nil diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index 2f572cdf54c..159f41c9f3c 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -19,9 +19,7 @@ import ( "k8s.io/kube-openapi/pkg/spec3" authlib "github.com/grafana/authlib/types" - "github.com/grafana/grafana-app-sdk/logging" - folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/apps/iam/pkg/reconcilers" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -49,21 +47,21 @@ var errNoResource = errors.New("resource name is required") // This is used just so wire has something unique to return type FolderAPIBuilder struct { - features featuremgmt.FeatureToggles - namespacer request.NamespaceMapper + features featuremgmt.FeatureToggles + namespacer request.NamespaceMapper + storage grafanarest.Storage + permissionStore reconcilers.PermissionStore + authorizer authorizer.Authorizer + accessClient authlib.AccessClient + parents parentsGetter + searcher resourcepb.ResourceIndexClient + permissionsOnCreate bool + + // Legacy services -- these will not exist in the MT environment folderSvc folder.LegacyService folderPermissionsSvc accesscontrol.FolderPermissionsService acService accesscontrol.Service ac accesscontrol.AccessControl - storage grafanarest.Storage - permissionStore reconcilers.PermissionStore - - authorizer authorizer.Authorizer - parents parentsGetter - - searcher resourcepb.ResourceIndexClient - permissionsOnCreate bool - ignoreLegacy bool // skip legacy storage and only use unified storage } func RegisterAPIService(cfg *setting.Cfg, @@ -85,6 +83,7 @@ func RegisterAPIService(cfg *setting.Cfg, folderPermissionsSvc: folderPermissionsSvc, acService: acService, ac: accessControl, + accessClient: accessClient, permissionsOnCreate: cfg.RBAC.PermissionsOnCreation("folder"), authorizer: newLegacyAuthorizer(accessControl), searcher: unified, @@ -99,10 +98,10 @@ func NewAPIService(ac authlib.AccessClient, searcher resource.ResourceClient, fe features: features, authorizer: newMultiTenantAuthorizer(ac), searcher: searcher, - ignoreLegacy: true, permissionStore: reconcilers.NewZanzanaPermissionStore(zanzanaClient), } } + func (b *FolderAPIBuilder) GetGroupVersion() schema.GroupVersion { return resourceInfo.GroupVersion() } @@ -142,77 +141,60 @@ func (b *FolderAPIBuilder) AllowedV0Alpha1Resources() []string { } func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { - scheme := opts.Scheme - optsGetter := opts.OptsGetter - dualWriteBuilder := opts.DualWriteBuilder - storage := map[string]rest.Storage{} - - if b.ignoreLegacy { - opts.StorageOptsRegister(resourceInfo.GroupResource(), apistore.StorageOptions{ - EnableFolderSupport: true, - RequireDeprecatedInternalID: true}) - - store, err := grafanaregistry.NewRegistryStore(opts.Scheme, resourceInfo, opts.OptsGetter) - if err != nil { - return err - } - b.registerPermissionHooks(store) - storage[resourceInfo.StoragePath()] = store - apiGroupInfo.VersionedResourcesStorageMap[folders.VERSION] = storage - b.storage = storage[resourceInfo.StoragePath()].(grafanarest.Storage) - b.parents = newParentsGetter(store, folder.MaxNestedFolderDepth) - return nil - } - - legacyStore := &legacyStorage{ - service: b.folderSvc, - namespacer: b.namespacer, - tableConverter: resourceInfo.TableConverter(), - } - opts.StorageOptsRegister(resourceInfo.GroupResource(), apistore.StorageOptions{ EnableFolderSupport: true, RequireDeprecatedInternalID: true}) - folderStore := &folderStorage{ - tableConverter: resourceInfo.TableConverter(), - folderPermissionsSvc: b.folderPermissionsSvc, - acService: b.acService, - permissionsOnCreate: b.permissionsOnCreate, + unified, err := grafanaregistry.NewRegistryStore(opts.Scheme, resourceInfo, opts.OptsGetter) + if err != nil { + return err } + b.registerPermissionHooks(unified) + b.storage = unified - if optsGetter != nil && dualWriteBuilder != nil { - store, err := grafanaregistry.NewRegistryStore(scheme, resourceInfo, optsGetter) + if b.folderSvc != nil { + legacyStore := &legacyStorage{ + service: b.folderSvc, + namespacer: b.namespacer, + tableConverter: resourceInfo.TableConverter(), + } + dw, err := opts.DualWriteBuilder(resourceInfo.GroupResource(), legacyStore, unified) if err != nil { return err } - - b.registerPermissionHooks(store) - - dw, err := dualWriteBuilder(resourceInfo.GroupResource(), legacyStore, store) - if err != nil { - return err + b.storage = &folderStorage{ + tableConverter: resourceInfo.TableConverter(), + folderPermissionsSvc: b.folderPermissionsSvc, + acService: b.acService, + permissionsOnCreate: b.permissionsOnCreate, + store: dw, } - - folderStore.store = dw } - storage[resourceInfo.StoragePath()] = folderStore - b.parents = newParentsGetter(folderStore, folder.MaxNestedFolderDepth) // used for validation + storage := map[string]rest.Storage{} + storage[resourceInfo.StoragePath()] = b.storage + + b.parents = newParentsGetter(b.storage, folder.MaxNestedFolderDepth) // used for validation storage[resourceInfo.StoragePath("parents")] = &subParentsREST{ - getter: folderStore, + getter: b.storage, parents: b.parents, } - storage[resourceInfo.StoragePath("counts")] = &subCountREST{searcher: b.searcher} - storage[resourceInfo.StoragePath("access")] = &subAccessREST{folderStore, b.ac} + storage[resourceInfo.StoragePath("counts")] = &subCountREST{ + getter: b.storage, + searcher: b.searcher, + } + storage[resourceInfo.StoragePath("access")] = &subAccessREST{ + getter: b.storage, + accessClient: b.accessClient, + } // Adds a path to return children of a given folder storage[resourceInfo.StoragePath("children")] = &subChildrenREST{ - lister: storage[resourceInfo.StoragePath()].(rest.Lister), + getter: b.storage, + lister: b.storage, } apiGroupInfo.VersionedResourcesStorageMap[folders.VERSION] = storage - b.storage = storage[resourceInfo.StoragePath()].(grafanarest.Storage) return nil } diff --git a/pkg/registry/apis/folders/sub_access.go b/pkg/registry/apis/folders/sub_access.go index 2b6bb443252..ff068a14a33 100644 --- a/pkg/registry/apis/folders/sub_access.go +++ b/pkg/registry/apis/folders/sub_access.go @@ -8,22 +8,23 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" + authlib "github.com/grafana/authlib/types" + foldersV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" ) type subAccessREST struct { - getter rest.Getter - ac accesscontrol.AccessControl + getter rest.Getter + accessClient authlib.AccessClient } var _ = rest.Connecter(&subAccessREST{}) var _ = rest.StorageMetadata(&subAccessREST{}) func (r *subAccessREST) New() runtime.Object { - return &folders.FolderAccessInfo{} + return &foldersV1.FolderAccessInfo{} } func (r *subAccessREST) Destroy() { @@ -38,7 +39,7 @@ func (r *subAccessREST) ProducesMIMETypes(verb string) []string { } func (r *subAccessREST) ProducesObject(verb string) interface{} { - return &folders.FolderAccessInfo{} + return &foldersV1.FolderAccessInfo{} } func (r *subAccessREST) NewConnectOptions() (runtime.Object, bool, string) { @@ -46,29 +47,67 @@ func (r *subAccessREST) NewConnectOptions() (runtime.Object, bool, string) { } func (r *subAccessREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + access, err := r.getAccessInfo(ctx, name) + if err != nil { + responder.Error(err) + } else { + responder.Object(200, access) + } + }), nil +} + +func (r *subAccessREST) getAccessInfo(ctx context.Context, name string) (*foldersV1.FolderAccessInfo, error) { + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } user, err := identity.GetRequester(ctx) if err != nil { return nil, err } - // Must be able to get the resource - _, err = r.getter.Get(ctx, name, &v1.GetOptions{}) + // Can view is managed here (and in the Authorizer) + f, err := r.getter.Get(ctx, name, &v1.GetOptions{}) if err != nil { return nil, err } + obj, err := utils.MetaAccessor(f) + if err != nil { + return nil, err + } + var tmp authlib.CheckResponse + check := func(verb string) bool { + if err != nil { + return false + } + tmp, err = r.accessClient.Check(ctx, user, authlib.CheckRequest{ + Verb: verb, + Group: foldersV1.GROUP, + Resource: foldersV1.RESOURCE, + Namespace: ns.Value, + Name: name, + Folder: obj.GetFolder(), + }) + return tmp.Allowed + } - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - access := &folders.FolderAccessInfo{} - canEditEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(name)) - access.CanEdit, _ = r.ac.Evaluate(ctx, user, canEditEvaluator) - access.CanSave = access.CanEdit - canAdminEvaluator := accesscontrol.EvalAll( - accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(name)), - accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(name)), - ) - access.CanAdmin, _ = r.ac.Evaluate(ctx, user, canAdminEvaluator) - canDeleteEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(name)) - access.CanDelete, _ = r.ac.Evaluate(ctx, user, canDeleteEvaluator) - responder.Object(http.StatusOK, access) - }), nil + rsp := &foldersV1.FolderAccessInfo{} + rsp.CanAdmin = check(utils.VerbSetPermissions) + if err != nil { + return nil, err + } + rsp.CanDelete = rsp.CanAdmin || check(utils.VerbDelete) + if err != nil { + return nil, err + } + rsp.CanEdit = rsp.CanAdmin || check(utils.VerbUpdate) + if err != nil { + return nil, err + } + rsp.CanSave = rsp.CanAdmin || check(utils.VerbCreate) // or the same as update? + if err != nil { + return nil, err + } + return rsp, nil } diff --git a/pkg/registry/apis/folders/sub_children.go b/pkg/registry/apis/folders/sub_children.go index 13dccda8d73..4509cc9273a 100644 --- a/pkg/registry/apis/folders/sub_children.go +++ b/pkg/registry/apis/folders/sub_children.go @@ -6,6 +6,7 @@ import ( "net/http" "k8s.io/apimachinery/pkg/apis/meta/internalversion" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" @@ -15,6 +16,7 @@ import ( ) type subChildrenREST struct { + getter rest.Getter lister rest.Lister } @@ -45,6 +47,10 @@ func (r *subChildrenREST) NewConnectOptions() (runtime.Object, bool, string) { } func (r *subChildrenREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + if _, err := r.getter.Get(ctx, name, &v1.GetOptions{}); err != nil { + return nil, err + } + obj, err := r.lister.List(ctx, &internalversion.ListOptions{ Limit: 500, // TODO, field selector diff --git a/pkg/registry/apis/folders/sub_count.go b/pkg/registry/apis/folders/sub_count.go index ebaeb590a50..9b68f3f063f 100644 --- a/pkg/registry/apis/folders/sub_count.go +++ b/pkg/registry/apis/folders/sub_count.go @@ -4,6 +4,7 @@ import ( "context" "net/http" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" @@ -13,6 +14,7 @@ import ( ) type subCountREST struct { + getter rest.Getter searcher resourcepb.ResourceIndexClient } @@ -45,6 +47,9 @@ func (r *subCountREST) NewConnectOptions() (runtime.Object, bool, string) { } func (r *subCountREST) Connect(ctx context.Context, name string, _ runtime.Object, responder rest.Responder) (http.Handler, error) { + if _, err := r.getter.Get(ctx, name, &v1.GetOptions{}); err != nil { + return nil, err + } return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { diff --git a/pkg/registry/apis/folders/sub_parents.go b/pkg/registry/apis/folders/sub_parents.go index 220921570d7..b0ab29592a9 100644 --- a/pkg/registry/apis/folders/sub_parents.go +++ b/pkg/registry/apis/folders/sub_parents.go @@ -8,7 +8,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - "k8s.io/apiserver/pkg/storage" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/services/folder" @@ -55,10 +54,6 @@ func (r *subParentsREST) Connect(ctx context.Context, name string, opts runtime. } obj, err := r.getter.Get(ctx, name, &metav1.GetOptions{}) - if storage.IsNotFound(err) { - responder.Object(http.StatusNotFound, nil) - return - } if err != nil { responder.Error(err) return diff --git a/pkg/registry/apis/iam/legacy/display_query.sql b/pkg/registry/apis/iam/legacy/display_query.sql index 15dfff35d09..3e8c1e33112 100644 --- a/pkg/registry/apis/iam/legacy/display_query.sql +++ b/pkg/registry/apis/iam/legacy/display_query.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM {{ .Ident .UserTable }} as u JOIN {{ .Ident .OrgUserTable }} as o ON u.id = o.user_id WHERE o.org_id = {{ .Arg .Query.OrgID }} AND ( 1=2 {{ if .Query.UIDs }} diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids.sql index 66f977fe3db..7354603168a 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR u.id IN (1, 2) diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids_uids.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids_uids.sql index 933d45adbee..ec1927fe7c6 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids_uids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_ids_uids.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR uid IN ('a', 'b') diff --git a/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_uids.sql b/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_uids.sql index a1c7c11dcad..6353dd6f6b9 100755 --- a/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_uids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/mysql--display_query-display_uids.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM `grafana`.`user` as u JOIN `grafana`.`org_user` as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR uid IN ('a', 'b') diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids.sql index d3cb1fb1e8c..0d2ccd8befa 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR u.id IN (1, 2) diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids_uids.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids_uids.sql index dfb4ed5afe2..a1514263169 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids_uids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_ids_uids.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR uid IN ('a', 'b') diff --git a/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_uids.sql b/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_uids.sql index a6989bb94e0..66a204fc20d 100755 --- a/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_uids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/postgres--display_query-display_uids.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR uid IN ('a', 'b') diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids.sql index d3cb1fb1e8c..0d2ccd8befa 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR u.id IN (1, 2) diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids_uids.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids_uids.sql index dfb4ed5afe2..a1514263169 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids_uids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_ids_uids.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR uid IN ('a', 'b') diff --git a/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_uids.sql b/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_uids.sql index a6989bb94e0..66a204fc20d 100755 --- a/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_uids.sql +++ b/pkg/registry/apis/iam/legacy/testdata/sqlite--display_query-display_uids.sql @@ -1,6 +1,6 @@ SELECT o.org_id, u.id, u.uid, u.login, u.email, u.name, u.created, u.updated, u.is_service_account, u.is_disabled, u.is_admin, - u.email_verified, u.is_provisioned, u.last_seen_at + u.email_verified, u.is_provisioned, u.last_seen_at, o.role FROM "grafana"."user" as u JOIN "grafana"."org_user" as o ON u.id = o.user_id WHERE o.org_id = 2 AND ( 1=2 OR uid IN ('a', 'b') diff --git a/pkg/registry/apis/iam/resourcepermission/sql.go b/pkg/registry/apis/iam/resourcepermission/sql.go index 034c438d2ae..d635ef2db09 100644 --- a/pkg/registry/apis/iam/resourcepermission/sql.go +++ b/pkg/registry/apis/iam/resourcepermission/sql.go @@ -516,6 +516,16 @@ func validateCreateAndUpdateInput(v0ResourcePerm *v0alpha1.ResourcePermission, g return fmt.Errorf("resource permission name does not match spec: %w", errInvalidSpec) } + // Check for duplicate entities (same kind and name should appear only once) + seen := make(map[string]bool) + for _, perm := range v0ResourcePerm.Spec.Permissions { + key := fmt.Sprintf("%s:%s", perm.Kind, perm.Name) + if seen[key] { + return fmt.Errorf("duplicate entity found: kind=%s, name=%s (each entity can only appear once per resource): %w", perm.Kind, perm.Name, errInvalidSpec) + } + seen[key] = true + } + return nil } diff --git a/pkg/registry/apis/iam/resourcepermission/sql_test.go b/pkg/registry/apis/iam/resourcepermission/sql_test.go index c5a1d8feef9..8ff91859d9e 100644 --- a/pkg/registry/apis/iam/resourcepermission/sql_test.go +++ b/pkg/registry/apis/iam/resourcepermission/sql_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "github.com/grafana/authlib/types" @@ -668,3 +669,181 @@ func (f *fakeIdentityStore) GetUserInternalID(ctx context.Context, ns types.Name } return &legacy.GetUserInternalIDResult{ID: id}, nil } + +func TestValidateCreateAndUpdateInput(t *testing.T) { + grn := &groupResourceName{ + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: "test-dashboard", + } + + t.Run("Should pass validation with valid permissions", func(t *testing.T) { + resourcePerm := &v0alpha1.ResourcePermission{ + Spec: v0alpha1.ResourcePermissionSpec{ + Resource: v0alpha1.ResourcePermissionspecResource{ + ApiGroup: "dashboard.grafana.app", + Resource: "dashboards", + Name: "test-dashboard", + }, + Permissions: []v0alpha1.ResourcePermissionspecPermission{ + { + Kind: v0alpha1.ResourcePermissionSpecPermissionKindBasicRole, + Name: "Editor", + Verb: "edit", + }, + { + Kind: v0alpha1.ResourcePermissionSpecPermissionKindBasicRole, + Name: "Viewer", // Different entity name - should be allowed + Verb: "view", + }, + { + Kind: v0alpha1.ResourcePermissionSpecPermissionKindUser, + Name: "user-1", + Verb: "edit", // Different kind - should be allowed + }, + }, + }, + } + + err := validateCreateAndUpdateInput(resourcePerm, grn) + require.NoError(t, err) + }) + + t.Run("Should fail validation with duplicate entities", func(t *testing.T) { + resourcePerm := &v0alpha1.ResourcePermission{ + Spec: v0alpha1.ResourcePermissionSpec{ + Resource: v0alpha1.ResourcePermissionspecResource{ + ApiGroup: "dashboard.grafana.app", + Resource: "dashboards", + Name: "test-dashboard", + }, + Permissions: []v0alpha1.ResourcePermissionspecPermission{ + { + Kind: v0alpha1.ResourcePermissionSpecPermissionKindBasicRole, + Name: "Editor", + Verb: "edit", + }, + { + Kind: v0alpha1.ResourcePermissionSpecPermissionKindBasicRole, + Name: "Editor", + Verb: "view", // Same entity, different verb - should fail + }, + }, + }, + } + + err := validateCreateAndUpdateInput(resourcePerm, grn) + require.Error(t, err) + require.Contains(t, err.Error(), "duplicate entity found") + require.Contains(t, err.Error(), "kind=BasicRole") + require.Contains(t, err.Error(), "name=Editor") + require.Contains(t, err.Error(), "each entity can only appear once per resource") + }) + + t.Run("Should pass validation with same name but different kinds", func(t *testing.T) { + resourcePerm := &v0alpha1.ResourcePermission{ + Spec: v0alpha1.ResourcePermissionSpec{ + Resource: v0alpha1.ResourcePermissionspecResource{ + ApiGroup: "dashboard.grafana.app", + Resource: "dashboards", + Name: "test-dashboard", + }, + Permissions: []v0alpha1.ResourcePermissionspecPermission{ + { + Kind: v0alpha1.ResourcePermissionSpecPermissionKindUser, + Name: "editor", // Same name but different kind + Verb: "edit", + }, + { + Kind: v0alpha1.ResourcePermissionSpecPermissionKindBasicRole, + Name: "editor", // Same name but different kind + Verb: "view", + }, + }, + }, + } + + err := validateCreateAndUpdateInput(resourcePerm, grn) + require.NoError(t, err) + }) +} + +func TestIntegration_UpdateResourcePermission_VerbChange(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + backend := setupBackend(t) + backend.identityStore = NewFakeIdentityStore(t) + ctx := context.Background() + sql, err := backend.dbProvider(ctx) + require.NoError(t, err) + setupTestRoles(t, sql.DB) + + mapper := backend.mappers[schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"}] + grn := &groupResourceName{Group: "dashboard.grafana.app", Resource: "dashboards", Name: "test-dash"} + + t.Run("should allow changing verb for same entity", func(t *testing.T) { + //Create initial permission with BasicRole Editor having "edit" verb + initialResourcePerm := &v0alpha1.ResourcePermission{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dashboard.grafana.app-dashboards-test-dash", + Namespace: "default", + }, + Spec: v0alpha1.ResourcePermissionSpec{ + Resource: v0alpha1.ResourcePermissionspecResource{ + ApiGroup: "dashboard.grafana.app", + Resource: "dashboards", + Name: "test-dash", + }, + Permissions: []v0alpha1.ResourcePermissionspecPermission{ + { + Kind: v0alpha1.ResourcePermissionSpecPermissionKindBasicRole, + Name: "Editor", + Verb: "edit", + }, + }, + }, + } + + rv, err := backend.createResourcePermission(ctx, sql, types.NamespaceInfo{Value: "default", OrgID: 1}, mapper, grn, initialResourcePerm) + require.NoError(t, err) + require.Greater(t, rv, int64(0)) + + //Update the same entity (BasicRole Editor) to have "view" verb instead + updatedResourcePerm := &v0alpha1.ResourcePermission{ + ObjectMeta: metav1.ObjectMeta{ + Name: "dashboard.grafana.app-dashboards-test-dash", + Namespace: "default", + }, + Spec: v0alpha1.ResourcePermissionSpec{ + Resource: v0alpha1.ResourcePermissionspecResource{ + ApiGroup: "dashboard.grafana.app", + Resource: "dashboards", + Name: "test-dash", + }, + Permissions: []v0alpha1.ResourcePermissionspecPermission{ + { + Kind: v0alpha1.ResourcePermissionSpecPermissionKindBasicRole, + Name: "Editor", + Verb: "view", + }, + }, + }, + } + + rv, err = backend.updateResourcePermission(ctx, sql, types.NamespaceInfo{Value: "default", OrgID: 1}, mapper, grn, updatedResourcePerm) + require.NoError(t, err) + require.Greater(t, rv, int64(0)) + + //Verify the update worked by reading back the permission + err = sql.DB.GetSqlxSession().WithTransaction(ctx, func(tx *session.SessionTx) error { + finalResourcePerm, err := backend.getResourcePermission(ctx, sql, tx, types.NamespaceInfo{Value: "default", OrgID: 1}, grn.string()) + require.NoError(t, err) + require.Len(t, finalResourcePerm.Spec.Permissions, 1) + require.Equal(t, v0alpha1.ResourcePermissionSpecPermissionKindBasicRole, finalResourcePerm.Spec.Permissions[0].Kind) + require.Equal(t, "Editor", finalResourcePerm.Spec.Permissions[0].Name) + require.Equal(t, "view", finalResourcePerm.Spec.Permissions[0].Verb) + return nil + }) + require.NoError(t, err) + }) +} diff --git a/pkg/registry/apis/iam/resourcepermission/storage_backend.go b/pkg/registry/apis/iam/resourcepermission/storage_backend.go index 24f0f982823..97d252a6fd5 100644 --- a/pkg/registry/apis/iam/resourcepermission/storage_backend.go +++ b/pkg/registry/apis/iam/resourcepermission/storage_backend.go @@ -95,7 +95,7 @@ func (s *ResourcePermSqlBackend) ListIterator(ctx context.Context, req *resource } pagination := &common.Pagination{ - Limit: limit, + Limit: limit + 1, // fetch one more to determine if there is a next page Continue: token.offset, } diff --git a/pkg/registry/apis/iam/resourcepermission/storage_backend_test.go b/pkg/registry/apis/iam/resourcepermission/storage_backend_test.go index e42dd8830e5..aac5eb5d38d 100644 --- a/pkg/registry/apis/iam/resourcepermission/storage_backend_test.go +++ b/pkg/registry/apis/iam/resourcepermission/storage_backend_test.go @@ -580,6 +580,9 @@ func TestIntegration_ResourcePermSqlBackend_ListIterator(t *testing.T) { continueToken = it.ContinueToken() require.NoError(t, json.Unmarshal(it.Value(), &perm)) results = append(results, perm.Name) + if len(results) >= int(req.Limit) { + return nil + } } return nil }) @@ -591,6 +594,9 @@ func TestIntegration_ResourcePermSqlBackend_ListIterator(t *testing.T) { continueToken = it.ContinueToken() require.NoError(t, json.Unmarshal(it.Value(), &perm)) results = append(results, perm.Name) + if len(results) >= int(req.Limit) { + return nil + } } return nil }) @@ -603,6 +609,9 @@ func TestIntegration_ResourcePermSqlBackend_ListIterator(t *testing.T) { continueToken = it.ContinueToken() require.NoError(t, json.Unmarshal(it.Value(), &perm)) results = append(results, perm.Name) + if len(results) >= int(req.Limit) { + return nil + } } return nil }) diff --git a/pkg/registry/apis/preferences/legacy/stars.go b/pkg/registry/apis/preferences/legacy/stars.go index 24b7290f2c3..26b6a5d574a 100644 --- a/pkg/registry/apis/preferences/legacy/stars.go +++ b/pkg/registry/apis/preferences/legacy/stars.go @@ -8,6 +8,7 @@ import ( "strings" "time" + apiserrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -126,21 +127,16 @@ func (s *DashboardStarsStorage) Get(ctx context.Context, name string, options *m } found, _, err := s.sql.GetStars(ctx, ns.OrgID, owner.Name) - if err != nil || len(found) == 0 { + if err != nil { return nil, err } + if len(found) == 0 || len(found[0].Dashboards) == 0 { + return nil, apiserrors.NewNotFound(preferences.StarsResourceInfo.GroupResource(), name) + } obj := asStarsResource(ns.Value, &found[0]) return &obj, nil } -func (s *DashboardStarsStorage) StarDashboard(ctx context.Context, name string, uid string) (runtime.Object, error) { - return nil, fmt.Errorf("TODO") -} - -func (s *DashboardStarsStorage) UnstarDashboard(ctx context.Context, name string, uid string) (runtime.Object, error) { - return nil, fmt.Errorf("TODO") -} - func getDashboardStars(stars *preferences.Stars) []string { if stars == nil || len(stars.Spec.Resource) == 0 { return []string{} @@ -180,12 +176,19 @@ func (s *DashboardStarsStorage) write(ctx context.Context, obj *preferences.Star }}, err } + current, _, err := s.sql.GetStars(ctx, ns.OrgID, owner.Name) + if err != nil { + return nil, err + } + changed := false now := time.Now() randID := now.UnixNano() + rand.Int63n(5000) previous := make(map[string]bool) - for _, v := range getDashboardStars(obj) { - previous[v] = true + if len(current) > 0 { + for _, v := range current[0].Dashboards { + previous[v] = true + } } for _, dashboard := range stars { if previous[dashboard] { diff --git a/pkg/registry/apis/preferences/register.go b/pkg/registry/apis/preferences/register.go index 704d314cd26..da6d2740ae5 100644 --- a/pkg/registry/apis/preferences/register.go +++ b/pkg/registry/apis/preferences/register.go @@ -14,6 +14,7 @@ import ( preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/registry/apis/preferences/legacy" "github.com/grafana/grafana/pkg/services/apiserver/builder" @@ -89,21 +90,23 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI storage := map[string]rest.Storage{} // Configure Stars Dual writer - stars := preferences.StarsResourceInfo - unified, err := grafanaregistry.NewRegistryStore(opts.Scheme, stars, opts.OptsGetter) + resource := preferences.StarsResourceInfo + var stars grafanarest.Storage + unified, err := grafanaregistry.NewRegistryStore(opts.Scheme, resource, opts.OptsGetter) if err != nil { return err } - storage[stars.StoragePath()] = unified - if b.stars != nil { + stars = unified + if b.stars != nil && opts.DualWriteBuilder != nil { legacy := legacy.NewDashboardStarsStorage(b.stars, b.users, b.namespacer, b.sql) - storage[stars.StoragePath()], err = opts.DualWriteBuilder(stars.GroupResource(), legacy, unified) + stars, err = opts.DualWriteBuilder(resource.GroupResource(), legacy, unified) if err != nil { return err } - storage[stars.StoragePath("write")] = &starsREST{ - store: legacy, // TODO, only supports legacy right now - } + } + storage[resource.StoragePath()] = stars + storage[resource.StoragePath("write")] = &starsREST{ + store: stars, } // Configure Preferences diff --git a/pkg/registry/apis/preferences/stars.go b/pkg/registry/apis/preferences/stars.go index 61d9cbd7f81..e082e8883f7 100644 --- a/pkg/registry/apis/preferences/stars.go +++ b/pkg/registry/apis/preferences/stars.go @@ -4,14 +4,18 @@ import ( "context" "fmt" "net/http" + "slices" "strings" apierrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1" - "github.com/grafana/grafana/pkg/registry/apis/preferences/legacy" + "github.com/grafana/grafana/pkg/apimachinery/identity" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/registry/apis/preferences/utils" ) type starItem struct { @@ -21,7 +25,7 @@ type starItem struct { } type starsREST struct { - store *legacy.DashboardStarsStorage + store grafanarest.Storage } var ( @@ -33,8 +37,7 @@ func (r *starsREST) New() runtime.Object { return &preferences.Stars{} } -func (r *starsREST) Destroy() { -} +func (r *starsREST) Destroy() {} func (r *starsREST) ConnectMethods() []string { return []string{"PUT", "DELETE"} @@ -53,6 +56,18 @@ func (r *starsREST) NewConnectOptions() (runtime.Object, bool, string) { } func (r *starsREST) Connect(ctx context.Context, name string, _ runtime.Object, responder rest.Responder) (http.Handler, error) { + user, err := identity.GetRequester(ctx) + if err != nil { + return nil, fmt.Errorf("must be logged in") + } + parsed, found := utils.ParseOwnerFromName(name) + if !found || parsed.Owner != utils.UserResourceOwner { + return nil, fmt.Errorf("only works with user stars") + } + if user.GetIdentifier() != parsed.Name { + return nil, fmt.Errorf("must request as the given user") + } + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { item, err := itemFromPath(req.URL.Path, fmt.Sprintf("/%s/write", name)) if err != nil { @@ -60,25 +75,62 @@ func (r *starsREST) Connect(ctx context.Context, name string, _ runtime.Object, return } - if item.group != "dashboard.grafana.app" || item.kind != "Dashboard" { - responder.Error(fmt.Errorf("only dashboards are supported right now")) + remove := false + switch req.Method { + case "DELETE": + remove = true + case "PUT": + remove = false + default: + responder.Error(apierrors.NewMethodNotSupported(preferences.PreferencesResourceInfo.GroupResource(), req.Method)) return } - var obj runtime.Object - switch req.Method { - case "DELETE": - obj, err = r.store.UnstarDashboard(ctx, name, item.id) - case "PUT": - obj, err = r.store.StarDashboard(ctx, name, item.id) - default: - err = fmt.Errorf("unsupported method") + current, err := r.store.Get(ctx, name, &v1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + if remove { + responder.Object(http.StatusNoContent, &v1.Status{ + Code: http.StatusNoContent, + }) + return + } + current = &preferences.Stars{ + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: user.GetNamespace(), + }, + } + } + } + + obj, ok := current.(*preferences.Stars) + if !ok { + responder.Error(fmt.Errorf("expected stars object")) + return + } + + if !apply(&obj.Spec, item, remove) { + responder.Object(http.StatusNoContent, &v1.Status{ + Code: http.StatusNoContent, + }) + return + } + + if len(obj.Spec.Resource) == 0 { + _, _, err = r.store.Delete(ctx, name, rest.ValidateAllObjectFunc, &v1.DeleteOptions{}) + } else if obj.ResourceVersion == "" { + _, err = r.store.Create(ctx, obj, rest.ValidateAllObjectFunc, &v1.CreateOptions{}) + } else { + _, _, err = r.store.Update(ctx, name, rest.DefaultUpdatedObjectInfo(obj), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, true, &v1.UpdateOptions{}) } if err != nil { responder.Error(err) return } - responder.Object(200, obj) + responder.Object(http.StatusOK, &v1.Status{ + Code: http.StatusOK, + }) }), nil } @@ -99,3 +151,49 @@ func itemFromPath(urlPath, prefix string) (starItem, error) { id: parts[2], }, nil } + +func apply(spec *preferences.StarsSpec, item starItem, remove bool) bool { + var stars *preferences.StarsResource + for idx, v := range spec.Resource { + if v.Group == item.group && v.Kind == item.kind { + stars = &spec.Resource[idx] + } + } + if stars == nil { + if remove { + return false + } + spec.Resource = append(spec.Resource, preferences.StarsResource{ + Group: item.group, + Kind: item.kind, + Names: []string{}, + }) + stars = &spec.Resource[len(spec.Resource)-1] + } + + idx := slices.Index(stars.Names, item.id) + if idx < 0 { // not found + if remove { + return false + } + stars.Names = append(stars.Names, item.id) + } else if remove { + stars.Names = append(stars.Names[:idx], stars.Names[idx+1:]...) + } else { + return false + } + slices.Sort(stars.Names) + + // Remove the slot if only one value + if len(stars.Names) == 0 { + tmp := preferences.StarsSpec{} + for _, v := range spec.Resource { + if v.Group == item.group && v.Kind == item.kind { + continue + } + tmp.Resource = append(tmp.Resource, v) + } + spec.Resource = tmp.Resource + } + return true +} diff --git a/pkg/registry/apis/preferences/stars_test.go b/pkg/registry/apis/preferences/stars_test.go new file mode 100644 index 00000000000..8faf413550c --- /dev/null +++ b/pkg/registry/apis/preferences/stars_test.go @@ -0,0 +1,234 @@ +package preferences + +import ( + "testing" + + "github.com/stretchr/testify/require" + + preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1" +) + +func TestStarsWrite(t *testing.T) { + t.Run("apply", func(t *testing.T) { + tests := []struct { + name string + spec preferences.StarsSpec + item starItem + remove bool + changed bool + expect preferences.StarsSpec + }{{ + name: "add to an existing array", + spec: preferences.StarsSpec{ + Resource: []preferences.StarsResource{{ + Group: "g", + Kind: "k", + Names: []string{"a", "b", "c"}, + }}, + }, + item: starItem{ + group: "g", + kind: "k", + id: "x", + }, + remove: false, + changed: true, + expect: preferences.StarsSpec{ + Resource: []preferences.StarsResource{{ + Group: "g", + Kind: "k", + Names: []string{"a", "b", "c", "x"}, // added "x" + }}, + }, + }, { + name: "remove from an existing array", + spec: preferences.StarsSpec{ + Resource: []preferences.StarsResource{{ + Group: "g", + Kind: "k", + Names: []string{"a", "b", "c"}, + }}, + }, + item: starItem{ + group: "g", + kind: "k", + id: "b", + }, + remove: true, + changed: true, + expect: preferences.StarsSpec{ + Resource: []preferences.StarsResource{{ + Group: "g", + Kind: "k", + Names: []string{"a", "c"}, // removed "b" + }}, + }, + }, { + name: "add to empty spec", + spec: preferences.StarsSpec{}, + item: starItem{ + group: "g", + kind: "k", + id: "a", + }, + remove: false, + changed: true, + expect: preferences.StarsSpec{ + Resource: []preferences.StarsResource{{ + Group: "g", + Kind: "k", + Names: []string{"a"}, + }}, + }, + }, { + name: "remove item that does not exist", + spec: preferences.StarsSpec{ + Resource: []preferences.StarsResource{{ + Group: "g", + Kind: "k", + Names: []string{"x"}, + }}, + }, + item: starItem{ + group: "g", + kind: "k", + id: "a", + }, + remove: true, + changed: false, + }, { + name: "add item that already exist", + spec: preferences.StarsSpec{ + Resource: []preferences.StarsResource{{ + Group: "g", + Kind: "k", + Names: []string{"x"}, + }}, + }, + item: starItem{ + group: "g", + kind: "k", + id: "x", + }, + remove: false, + changed: false, + }, { + name: "remove from empty", + spec: preferences.StarsSpec{}, + item: starItem{ + group: "g", + kind: "k", + id: "a", + }, + remove: true, + changed: false, + }, { + name: "remove item that does not exist", + spec: preferences.StarsSpec{ + Resource: []preferences.StarsResource{{ + Group: "g", + Kind: "k", + Names: []string{"a", "b", "c"}, + }}, + }, + item: starItem{ + group: "g", + kind: "k", + id: "X", + }, + remove: true, + changed: false, + }, { + name: "remove last item", + spec: preferences.StarsSpec{ + Resource: []preferences.StarsResource{{ + Group: "g", + Kind: "k", + Names: []string{"a"}, + }}, + }, + item: starItem{ + group: "g", + kind: "k", + id: "a", + }, + remove: true, + changed: true, + expect: preferences.StarsSpec{}, + }, { + name: "remove last item (with others)", + spec: preferences.StarsSpec{ + Resource: []preferences.StarsResource{{ + Group: "g", + Kind: "k", + Names: []string{"a"}, + }, { + Group: "g2", + Kind: "k2", + Names: []string{"a"}, + }}}, + item: starItem{ + group: "g", + kind: "k", + id: "a", + }, + remove: true, + changed: true, + expect: preferences.StarsSpec{ + Resource: []preferences.StarsResource{{ + Group: "g2", + Kind: "k2", + Names: []string{"a"}, + }}}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + changed := apply(&tt.spec, tt.item, tt.remove) + require.Equal(t, tt.changed, changed) + if changed { + require.Equal(t, tt.expect, tt.spec) + } + }) + } + }) + + t.Run("path", func(t *testing.T) { + tests := []struct { + name string + url string + prefix string + item starItem + err string + }{{ + name: "normal", + url: "http://localhost:3000/apis/preferences.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/Dashboard/000000127", + prefix: "/user-abc/write", + item: starItem{ + group: "dashboard.grafana.app", + kind: "Dashboard", + id: "000000127", + }, + }, { + name: "prefix not found", + url: "http://localhost:3000/apis/preferences.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/Dashboard/000000127", + prefix: "/something/write", + err: "invalid request path", + }, { + name: "missing three parts", + url: "http://localhost:3000/apis/preferences.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/000000127", + prefix: "/user-abc/write", + err: "expected {group}/{kind}/{id}", + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + item, err := itemFromPath(tt.url, tt.prefix) + if tt.err == "" { + require.NoError(t, err) + require.Equal(t, tt.item, item) + } else { + require.ErrorContains(t, err, tt.err) + } + }) + } + }) +} diff --git a/pkg/registry/apis/provisioning/controller/finalizers.go b/pkg/registry/apis/provisioning/controller/finalizers.go index 72c2209911c..952d11bf0f8 100644 --- a/pkg/registry/apis/provisioning/controller/finalizers.go +++ b/pkg/registry/apis/provisioning/controller/finalizers.go @@ -5,6 +5,7 @@ import ( "encoding/json" "sort" "strings" + "time" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -17,11 +18,13 @@ import ( "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + metricutils "github.com/grafana/grafana/pkg/registry/apis/provisioning/utils" ) type finalizer struct { lister resources.ResourceLister clientFactory resources.ClientFactory + metrics *finalizerMetrics } func (f *finalizer) process(ctx context.Context, @@ -31,18 +34,24 @@ func (f *finalizer) process(ctx context.Context, logger := logging.FromContext(ctx) for _, finalizer := range finalizers { + var err error + var count int + start := time.Now() + outcome := metricutils.SuccessOutcome + switch finalizer { case repository.CleanFinalizer: // NOTE: the controller loop will never get run unless a finalizer is set hooks, ok := repo.(repository.Hooks) if ok { - if err := hooks.OnDelete(ctx); err != nil { + if err = hooks.OnDelete(ctx); err != nil { logger.Warn("Error running deletion hooks", "err", err) + outcome = metricutils.ErrorOutcome } } case repository.ReleaseOrphanResourcesFinalizer: - err := f.processExistingItems(ctx, repo.Config(), + count, err = f.processExistingItems(ctx, repo.Config(), func(client dynamic.ResourceInterface, item *provisioning.ResourceListItem) error { patchAnnotations, err := getPatchedAnnotations(item) if err != nil { @@ -55,20 +64,29 @@ func (f *finalizer) process(ctx context.Context, return err }) if err != nil { - return err + outcome = metricutils.ErrorOutcome + logger.Warn("Error processing release orphan resources finalizer", "err", err) } case repository.RemoveOrphanResourcesFinalizer: - err := f.processExistingItems(ctx, repo.Config(), + count, err = f.processExistingItems(ctx, repo.Config(), func(client dynamic.ResourceInterface, item *provisioning.ResourceListItem) error { return client.Delete(ctx, item.Name, v1.DeleteOptions{}) }) if err != nil { - return err + outcome = metricutils.ErrorOutcome + logger.Warn("Error processing remove orphan resources finalizer", "err", err) } default: logger.Warn("skipping unknown finalizer", "finalizer", finalizer) + continue + } + + f.metrics.RecordFinalizer(finalizer, outcome, count, time.Since(start).Seconds()) + + if err != nil { + return err } } return nil @@ -79,17 +97,17 @@ func (f *finalizer) processExistingItems( ctx context.Context, repo *provisioning.Repository, cb func(client dynamic.ResourceInterface, item *provisioning.ResourceListItem) error, -) error { +) (int, error) { logger := logging.FromContext(ctx) clients, err := f.clientFactory.Clients(ctx, repo.Namespace) if err != nil { - return err + return 0, err } items, err := f.lister.List(ctx, repo.Namespace, repo.Name) if err != nil { logger.Warn("error listing resources", "error", err) - return err + return 0, err } // Safe deletion order @@ -103,7 +121,7 @@ func (f *finalizer) processExistingItems( Resource: item.Resource, }) if err != nil { - return err + return count, err } err = cb(res, &item) @@ -115,7 +133,7 @@ func (f *finalizer) processExistingItems( } } logger.Info("processed orphan items", "items", count, "errors", errors) - return nil + return count, nil } type jsonPatchOperation struct { diff --git a/pkg/registry/apis/provisioning/controller/health.go b/pkg/registry/apis/provisioning/controller/health.go index f6bad8fff6b..cf3aeedc36d 100644 --- a/pkg/registry/apis/provisioning/controller/health.go +++ b/pkg/registry/apis/provisioning/controller/health.go @@ -5,8 +5,11 @@ import ( "fmt" "time" + "github.com/grafana/grafana-app-sdk/logging" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/utils" + "github.com/prometheus/client_golang/prometheus" ) // StatusPatcher defines the interface for updating repository status @@ -19,12 +22,15 @@ type StatusPatcher interface { // HealthChecker provides unified health checking for repositories type HealthChecker struct { statusPatcher StatusPatcher + healthMetrics healthMetrics } // NewHealthChecker creates a new health checker -func NewHealthChecker(statusPatcher StatusPatcher) *HealthChecker { +func NewHealthChecker(statusPatcher StatusPatcher, registry prometheus.Registerer) *HealthChecker { + healthMetrics := registerHealthMetrics(registry) return &HealthChecker{ statusPatcher: statusPatcher, + healthMetrics: healthMetrics, } } @@ -163,8 +169,17 @@ func (hc *HealthChecker) RefreshTimestamp(ctx context.Context, repo *provisionin // refreshHealth performs a comprehensive health check // Returns test results, health status, and any error func (hc *HealthChecker) refreshHealth(ctx context.Context, repo repository.Repository, existingStatus provisioning.HealthStatus) (*provisioning.TestResults, provisioning.HealthStatus, error) { + logger := logging.FromContext(ctx).With("repo", repo.Config().GetName(), "namespace", repo.Config().GetNamespace()) + start := time.Now() + outcome := utils.SuccessOutcome + defer func() { + hc.healthMetrics.RecordHealthCheck(outcome, time.Since(start).Seconds()) + }() + res, err := repository.TestRepository(ctx, repo) if err != nil { + outcome = utils.ErrorOutcome + logger.Error("failed to test repository", "error", err) return nil, existingStatus, fmt.Errorf("failed to test repository: %w", err) } diff --git a/pkg/registry/apis/provisioning/controller/health_test.go b/pkg/registry/apis/provisioning/controller/health_test.go index a42cd35c7aa..74f8a127be6 100644 --- a/pkg/registry/apis/provisioning/controller/health_test.go +++ b/pkg/registry/apis/provisioning/controller/health_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -18,7 +19,7 @@ import ( func TestNewHealthChecker(t *testing.T) { mockPatcher := mocks.NewStatusPatcher(t) - hc := NewHealthChecker(mockPatcher) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) assert.NotNil(t, hc) assert.Equal(t, mockPatcher, hc.statusPatcher) @@ -135,7 +136,7 @@ func TestShouldCheckHealth(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockPatcher := mocks.NewStatusPatcher(t) - hc := NewHealthChecker(mockPatcher) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) result := hc.ShouldCheckHealth(tt.repo) assert.Equal(t, tt.expected, result) @@ -222,7 +223,7 @@ func TestHasRecentFailure(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockPatcher := mocks.NewStatusPatcher(t) - hc := NewHealthChecker(mockPatcher) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) result := hc.HasRecentFailure(tt.healthStatus, tt.failureType) assert.Equal(t, tt.expected, result) @@ -264,7 +265,7 @@ func TestRecordFailure(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockPatcher := mocks.NewStatusPatcher(t) - hc := NewHealthChecker(mockPatcher) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) repo := &provisioning.Repository{ Status: provisioning.RepositoryStatus{ @@ -309,7 +310,7 @@ func TestRecordFailure(t *testing.T) { func TestRecordFailureFunction(t *testing.T) { mockPatcher := mocks.NewStatusPatcher(t) - hc := NewHealthChecker(mockPatcher) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) testErr := errors.New("test error") result := hc.recordFailure(provisioning.HealthFailureHook, testErr) @@ -446,7 +447,7 @@ func TestRefreshHealth(t *testing.T) { testError: tt.testError, } - hc := NewHealthChecker(mockPatcher) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) if tt.expectPatch { if tt.patchError != nil { @@ -556,7 +557,7 @@ func TestHasHealthStatusChanged(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockPatcher := mocks.NewStatusPatcher(t) - hc := NewHealthChecker(mockPatcher) + hc := NewHealthChecker(mockPatcher, prometheus.NewPedanticRegistry()) result := hc.hasHealthStatusChanged(tt.old, tt.new) assert.Equal(t, tt.expected, result) diff --git a/pkg/registry/apis/provisioning/controller/metrics.go b/pkg/registry/apis/provisioning/controller/metrics.go new file mode 100644 index 00000000000..a6850b438c9 --- /dev/null +++ b/pkg/registry/apis/provisioning/controller/metrics.go @@ -0,0 +1,84 @@ +package controller + +import ( + "github.com/grafana/grafana/pkg/registry/apis/provisioning/utils" + "github.com/prometheus/client_golang/prometheus" +) + +type finalizerMetrics struct { + registry prometheus.Registerer + finalizerProcessedTotal *prometheus.CounterVec + finalizerDuration *prometheus.HistogramVec +} + +func registerFinalizerMetrics(registry prometheus.Registerer) finalizerMetrics { + finalizerProcessedTotal := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "grafana_provisioning_finalizers_processed_total", + Help: "Total number of finalizers processed", + }, + []string{"finalizer_type", "outcome"}, + ) + registry.MustRegister(finalizerProcessedTotal) + + finalizerDuration := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "grafana_provisioning_finalizers_duration_seconds", + Help: "Duration of processing the finalizers", + Buckets: []float64{0.5, 1.0, 2.0, 5.0, 10.0, 30.0}, + }, + []string{"finalizer_type", "resource_count_bucket"}, + ) + registry.MustRegister(finalizerDuration) + + return finalizerMetrics{ + registry: registry, + finalizerProcessedTotal: finalizerProcessedTotal, + finalizerDuration: finalizerDuration, + } +} + +func (m *finalizerMetrics) RecordFinalizer(finalizerType string, outcome string, resourceCountChanged int, duration float64) { + m.finalizerProcessedTotal.WithLabelValues(finalizerType, outcome).Inc() + if outcome == utils.SuccessOutcome { + m.finalizerDuration.WithLabelValues(finalizerType, utils.GetResourceCountBucket(resourceCountChanged)).Observe(duration) + } +} + +type healthMetrics struct { + registry prometheus.Registerer + healthCheckedTotal *prometheus.CounterVec + healthCheckedDuration *prometheus.HistogramVec +} + +func registerHealthMetrics(registry prometheus.Registerer) healthMetrics { + healthCheckedTotal := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "grafana_provisioning_health_checked_total", + Help: "Total number of health checks performed", + }, + []string{"outcome"}, + ) + registry.MustRegister(healthCheckedTotal) + + healthCheckedDuration := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "grafana_provisioning_health_checked_duration_seconds", + Help: "Duration of health checks", + Buckets: []float64{0.1, 0.2, 0.5, 1.0, 2.0, 5.0}, + }, + []string{}, + ) + registry.MustRegister(healthCheckedDuration) + + return healthMetrics{ + registry: registry, + healthCheckedTotal: healthCheckedTotal, + healthCheckedDuration: healthCheckedDuration, + } +} + +func (m *healthMetrics) RecordHealthCheck(outcome string, duration float64) { + m.healthCheckedTotal.WithLabelValues(outcome).Inc() + m.healthCheckedDuration.WithLabelValues().Observe(duration) +} diff --git a/pkg/registry/apis/provisioning/controller/repository.go b/pkg/registry/apis/provisioning/controller/repository.go index 1b59c5ac2ef..ccda76e1722 100644 --- a/pkg/registry/apis/provisioning/controller/repository.go +++ b/pkg/registry/apis/provisioning/controller/repository.go @@ -22,9 +22,11 @@ import ( listers "github.com/grafana/grafana/apps/provisioning/pkg/generated/listers/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + "github.com/prometheus/client_golang/prometheus" ) const loggerName = "provisioning-repository-controller" @@ -59,6 +61,9 @@ type RepositoryController struct { keyFunc func(obj any) (string, error) queue workqueue.TypedRateLimitingInterface[*queueItem] + + registry prometheus.Registerer + tracer tracing.Tracer } // NewRepositoryController creates new RepositoryController. @@ -72,7 +77,11 @@ func NewRepositoryController( dualwrite dualwrite.Service, healthChecker *HealthChecker, statusPatcher StatusPatcher, + registry prometheus.Registerer, + tracer tracing.Tracer, ) (*RepositoryController, error) { + finalizerMetrics := registerFinalizerMetrics(registry) + rc := &RepositoryController{ client: provisioningClient, repoLister: repoInformer.Lister(), @@ -89,10 +98,13 @@ func NewRepositoryController( finalizer: &finalizer{ lister: resourceLister, clientFactory: clients, + metrics: &finalizerMetrics, }, jobs: jobs, logger: logging.DefaultLogger.With("logger", loggerName), dualwrite: dualwrite, + registry: registry, + tracer: tracer, } _, err := repoInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ diff --git a/pkg/registry/apis/provisioning/jobs/concurrent_driver.go b/pkg/registry/apis/provisioning/jobs/concurrent_driver.go index 0504fc414d5..1fdc32bea60 100644 --- a/pkg/registry/apis/provisioning/jobs/concurrent_driver.go +++ b/pkg/registry/apis/provisioning/jobs/concurrent_driver.go @@ -7,6 +7,7 @@ import ( "time" "github.com/grafana/grafana-app-sdk/logging" + "github.com/prometheus/client_golang/prometheus" ) // ConcurrentJobDriver manages multiple jobDriver instances for concurrent job processing. @@ -31,6 +32,7 @@ func NewConcurrentJobDriver( repoGetter RepoGetter, historicJobs HistoryWriter, notifications chan struct{}, + registry prometheus.Registerer, workers ...Worker, ) (*ConcurrentJobDriver, error) { if numDrivers <= 0 { @@ -55,6 +57,8 @@ func NewConcurrentJobDriver( cleanupInterval = 5 * time.Minute // Maximum cleanup interval } + recordConcurrentDriverMetric(registry, numDrivers) + return &ConcurrentJobDriver{ numDrivers: numDrivers, jobTimeout: jobTimeout, diff --git a/pkg/registry/apis/provisioning/jobs/delete/worker.go b/pkg/registry/apis/provisioning/jobs/delete/worker.go index 11d78e73502..e06ae627917 100644 --- a/pkg/registry/apis/provisioning/jobs/delete/worker.go +++ b/pkg/registry/apis/provisioning/jobs/delete/worker.go @@ -8,23 +8,27 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/grafana/grafana-app-sdk/logging" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/utils" ) type Worker struct { syncWorker jobs.Worker wrapFn repository.WrapWithStageFn resourcesFactory resources.RepositoryResourcesFactory + metrics jobs.JobMetrics } -func NewWorker(syncWorker jobs.Worker, wrapFn repository.WrapWithStageFn, resourcesFactory resources.RepositoryResourcesFactory) *Worker { +func NewWorker(syncWorker jobs.Worker, wrapFn repository.WrapWithStageFn, resourcesFactory resources.RepositoryResourcesFactory, metrics jobs.JobMetrics) *Worker { return &Worker{ syncWorker: syncWorker, wrapFn: wrapFn, resourcesFactory: resourcesFactory, + metrics: metrics, } } @@ -37,8 +41,15 @@ func (w *Worker) Process(ctx context.Context, repo repository.Repository, job pr return errors.New("missing delete settings") } + logger := logging.FromContext(ctx).With("job", job.GetName(), "namespace", job.GetNamespace()) opts := *job.Spec.Delete paths := opts.Paths + start := time.Now() + outcome := utils.ErrorOutcome + resourcesDeleted := 0 + defer func() { + w.metrics.RecordJob(string(provisioning.JobActionDelete), outcome, resourcesDeleted, time.Since(start).Seconds()) + }() progress.SetTotal(ctx, len(paths)+len(opts.Resources)) progress.StrictMaxErrors(1) // Fail fast on any error during deletion @@ -46,6 +57,7 @@ func (w *Worker) Process(ctx context.Context, repo repository.Repository, job pr fn := func(repo repository.Repository, _ bool) error { rw, ok := repo.(repository.ReaderWriter) if !ok { + logger.Error("delete job submitted targeting repository that is not a ReaderWriter") return errors.New("delete job submitted targeting repository that is not a ReaderWriter") } @@ -53,6 +65,7 @@ func (w *Worker) Process(ctx context.Context, repo repository.Repository, job pr if len(opts.Resources) > 0 { resolvedPaths, err := w.resolveResourcesToPaths(ctx, rw, progress, opts.Resources) if err != nil { + logger.Error("failed to resolve resource paths", "error", err) return err } paths = append(paths, resolvedPaths...) @@ -75,6 +88,7 @@ func (w *Worker) Process(ctx context.Context, repo repository.Repository, job pr err := w.wrapFn(ctx, repo, stageOptions, fn) if err != nil { + logger.Error("failed to delete files from repository", "error", err) return fmt.Errorf("delete files from repository: %w", err) } @@ -101,10 +115,17 @@ func (w *Worker) Process(ctx context.Context, repo repository.Repository, job pr } if err := w.syncWorker.Process(ctx, repo, syncJob, progress); err != nil { + logger.Error("failed to pull resources", "error", err) return fmt.Errorf("pull resources: %w", err) } } + outcome = utils.SuccessOutcome + jobStatus := progress.Complete(ctx, nil) + for _, summary := range jobStatus.Summary { + resourcesDeleted += int(summary.Delete) + } + return nil } diff --git a/pkg/registry/apis/provisioning/jobs/delete/worker_test.go b/pkg/registry/apis/provisioning/jobs/delete/worker_test.go index 1bcfed8d4cc..07cc855f194 100644 --- a/pkg/registry/apis/provisioning/jobs/delete/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/delete/worker_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -37,6 +38,7 @@ func (s *simpleRepository) Test(ctx context.Context) (*v0alpha1.TestResults, err } func TestDeleteWorker_IsSupported(t *testing.T) { + metrics := jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()) tests := []struct { name string job v0alpha1.Job @@ -73,7 +75,7 @@ func TestDeleteWorker_IsSupported(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - worker := NewWorker(nil, nil, nil) + worker := NewWorker(nil, nil, nil, metrics) result := worker.IsSupported(context.Background(), tt.job) require.Equal(t, tt.expected, result) }) @@ -87,7 +89,7 @@ func TestDeleteWorker_ProcessMissingDeleteSettings(t *testing.T) { }, } - worker := NewWorker(nil, nil, nil) + worker := NewWorker(nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), nil, job, nil) require.EqualError(t, err, "missing delete settings") } @@ -116,7 +118,7 @@ func TestDeleteWorker_ProcessNotReaderWriter(t *testing.T) { mockProgress.On("SetTotal", mock.Anything, 1).Return() mockProgress.On("StrictMaxErrors", 1).Return() - worker := NewWorker(nil, mockWrapFn.Execute, nil) + worker := NewWorker(nil, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "delete files from repository: delete job submitted targeting repository that is not a ReaderWriter") } @@ -139,7 +141,7 @@ func TestDeleteWorker_ProcessWrapFnError(t *testing.T) { mockProgress.On("SetTotal", mock.Anything, 1).Return() mockProgress.On("StrictMaxErrors", 1).Return() - worker := NewWorker(nil, mockWrapFn.Execute, nil) + worker := NewWorker(nil, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "delete files from repository: stage failed") } @@ -176,6 +178,7 @@ func TestDeleteWorker_ProcessDeleteFilesSuccess(t *testing.T) { mockProgress.On("SetMessage", mock.Anything, "Deleting test/path1").Return() mockProgress.On("SetMessage", mock.Anything, "Deleting test/path2").Return() mockProgress.On("TooManyErrors").Return(nil).Twice() + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) mockRepo.On("Delete", mock.Anything, "test/path1", "main", "Delete test/path1").Return(nil) mockRepo.On("Delete", mock.Anything, "test/path2", "main", "Delete test/path2").Return(nil) @@ -187,7 +190,7 @@ func TestDeleteWorker_ProcessDeleteFilesSuccess(t *testing.T) { return result.Path == "test/path2" && result.Action == repository.FileActionDeleted && result.Error == nil })).Return() - worker := NewWorker(nil, mockWrapFn.Execute, nil) + worker := NewWorker(nil, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -225,7 +228,7 @@ func TestDeleteWorker_ProcessDeleteFilesWithError(t *testing.T) { })).Return() mockProgress.On("TooManyErrors").Return(errors.New("too many errors")) - worker := NewWorker(nil, mockWrapFn.Execute, nil) + worker := NewWorker(nil, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "delete files from repository: too many errors") } @@ -255,6 +258,7 @@ func TestDeleteWorker_ProcessWithSyncWorker(t *testing.T) { mockProgress.On("StrictMaxErrors", 1).Return() mockProgress.On("SetMessage", mock.Anything, "Deleting test/path").Return() mockProgress.On("TooManyErrors").Return(nil) + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) mockRepo.On("Delete", mock.Anything, "test/path", "", "Delete test/path").Return(nil) @@ -269,7 +273,7 @@ func TestDeleteWorker_ProcessWithSyncWorker(t *testing.T) { return syncJob.Spec.Pull != nil && !syncJob.Spec.Pull.Incremental }), mockProgress).Return(nil) - worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil) + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -309,7 +313,7 @@ func TestDeleteWorker_ProcessSyncWorkerError(t *testing.T) { syncError := errors.New("sync failed") mockSyncWorker.On("Process", mock.Anything, mockRepo, mock.Anything, mockProgress).Return(syncError) - worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil) + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "pull resources: sync failed") } @@ -378,7 +382,7 @@ func TestDeleteWorker_deleteFiles(t *testing.T) { } } - worker := NewWorker(nil, nil, nil) + worker := NewWorker(nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.deleteFiles(context.Background(), mockRepo, mockProgress, opts, tt.paths...) if tt.expectedError != "" { @@ -456,6 +460,7 @@ func TestDeleteWorker_ProcessWithResourceRefs(t *testing.T) { mockProgress.On("SetMessage", mock.Anything, "Deleting dashboards/test-dashboard.json").Return() mockProgress.On("SetMessage", mock.Anything, "Deleting folders/test-folder.json").Return() mockProgress.On("TooManyErrors").Return(nil).Times(3) + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) // Mock file deletions mockRepo.On("Delete", mock.Anything, "test/path1", "main", "Delete test/path1").Return(nil) @@ -473,7 +478,7 @@ func TestDeleteWorker_ProcessWithResourceRefs(t *testing.T) { return result.Path == "folders/test-folder.json" && result.Action == repository.FileActionDeleted && result.Error == nil })).Return() - worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) @@ -524,6 +529,7 @@ func TestDeleteWorker_ProcessResourceRefsOnly(t *testing.T) { mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/test-dashboard").Return() mockProgress.On("SetMessage", mock.Anything, "Deleting dashboards/test-dashboard.json").Return() mockProgress.On("TooManyErrors").Return(nil) + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) mockRepo.On("Delete", mock.Anything, "dashboards/test-dashboard.json", "main", "Delete dashboards/test-dashboard.json").Return(nil) @@ -531,7 +537,7 @@ func TestDeleteWorker_ProcessResourceRefsOnly(t *testing.T) { return result.Path == "dashboards/test-dashboard.json" && result.Action == repository.FileActionDeleted && result.Error == nil })).Return() - worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -577,6 +583,7 @@ func TestDeleteWorker_ProcessResourceResolutionError(t *testing.T) { mockProgress.On("StrictMaxErrors", 1).Return() mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/nonexistent-dashboard").Return() + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) // Expect error to be recorded, not thrown mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { @@ -596,7 +603,7 @@ func TestDeleteWorker_ProcessResourceResolutionError(t *testing.T) { return syncJob.Spec.Pull != nil && !syncJob.Spec.Pull.Incremental }), mockProgress).Return(nil) - worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) // Should succeed even with resource resolution error } @@ -635,7 +642,7 @@ func TestDeleteWorker_ProcessResourcesFactoryError(t *testing.T) { mockProgress.On("StrictMaxErrors", 1).Return() mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() - worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "delete files from repository: create repository resources client: failed to create repository resources client") } @@ -671,7 +678,7 @@ func TestDeleteWorker_ProcessResourceRefsNotReaderWriter(t *testing.T) { mockProgress.On("SetTotal", mock.Anything, 1).Return() mockProgress.On("StrictMaxErrors", 1).Return() - worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "delete files from repository: delete job submitted targeting repository that is not a ReaderWriter") } @@ -724,7 +731,7 @@ func TestDeleteWorker_ProcessResourceResolutionTooManyErrors(t *testing.T) { })).Return() mockProgress.On("TooManyErrors").Return(errors.New("too many errors")) - worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "delete files from repository: too many errors") } @@ -800,7 +807,7 @@ func TestDeleteWorker_ProcessMixedResourcesWithPartialFailure(t *testing.T) { mockProgress.On("SetMessage", mock.Anything, "Finding path for resource folder.grafana.app/Folder/valid-folder").Return() mockProgress.On("SetMessage", mock.Anything, "Deleting dashboards/valid-dashboard.json").Return() mockProgress.On("SetMessage", mock.Anything, "Deleting folders/valid-folder.json").Return() - + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) // Record the error for the failed resource mockProgress.On("Record", mock.Anything, mock.MatchedBy(func(result jobs.JobResourceResult) bool { return result.Name == "nonexistent-dashboard" && result.Error != nil @@ -821,7 +828,7 @@ func TestDeleteWorker_ProcessMixedResourcesWithPartialFailure(t *testing.T) { return result.Path == "folders/valid-folder.json" && result.Error == nil })).Return() - worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) // Should succeed overall, with only the failed resource recorded as error } @@ -874,7 +881,7 @@ func TestDeleteWorker_ProcessWithPathDeduplication(t *testing.T) { // Expect total of 5 items (2 explicit paths + 3 resources), but only 3 unique paths will be deleted mockProgress.On("SetTotal", mock.Anything, 5).Return() mockProgress.On("StrictMaxErrors", 1).Return() - + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) // Resource resolution phase mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() @@ -919,7 +926,7 @@ func TestDeleteWorker_ProcessWithPathDeduplication(t *testing.T) { return result.Path == "dashboards/unique-dashboard.json" && result.Action == repository.FileActionDeleted && result.Error == nil })).Return() - worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) @@ -1014,7 +1021,7 @@ func TestDeleteWorker_RefURLsSetWithRef(t *testing.T) { mockProgress.On("Record", mock.Anything, mock.Anything).Once() mockProgress.On("TooManyErrors").Return(nil).Once() mockProgress.On("SetRefURLs", mock.Anything, expectedRefURLs).Once() - + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) mockReaderWriter := repository.NewMockReaderWriter(t) mockReaderWriter.On("Delete", mock.Anything, "test.json", "feature-branch", "Delete test.json").Return(nil) @@ -1036,7 +1043,7 @@ func TestDeleteWorker_RefURLsSetWithRef(t *testing.T) { }, } - worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepoWithURLs, job, mockProgress) require.NoError(t, err) @@ -1066,6 +1073,7 @@ func TestDeleteWorker_RefURLsNotSetWithoutRef(t *testing.T) { mockProgress.On("TooManyErrors").Return(nil).Once() mockProgress.On("ResetResults").Once() mockProgress.On("SetMessage", mock.Anything, "pull resources").Once() + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) // SetRefURLs should NOT be called since no ref is specified mockReaderWriter := repository.NewMockReaderWriter(t) @@ -1092,7 +1100,7 @@ func TestDeleteWorker_RefURLsNotSetWithoutRef(t *testing.T) { }, } - worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepoWithURLs, job, mockProgress) require.NoError(t, err) @@ -1119,6 +1127,7 @@ func TestDeleteWorker_RefURLsNotSetForNonURLRepository(t *testing.T) { mockProgress.On("SetMessage", mock.Anything, "Deleting test.json").Once() mockProgress.On("Record", mock.Anything, mock.Anything).Once() mockProgress.On("TooManyErrors").Return(nil).Once() + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) // SetRefURLs should NOT be called since repo doesn't support URLs mockReaderWriter := repository.NewMockReaderWriter(t) @@ -1142,7 +1151,7 @@ func TestDeleteWorker_RefURLsNotSetForNonURLRepository(t *testing.T) { }, } - worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) diff --git a/pkg/registry/apis/provisioning/jobs/export/worker.go b/pkg/registry/apis/provisioning/jobs/export/worker.go index e7f5404d6cc..092243c4567 100644 --- a/pkg/registry/apis/provisioning/jobs/export/worker.go +++ b/pkg/registry/apis/provisioning/jobs/export/worker.go @@ -6,10 +6,12 @@ import ( "fmt" "time" + "github.com/grafana/grafana-app-sdk/logging" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/utils" ) //go:generate mockery --name ExportFn --structname MockExportFn --inpackage --filename mock_export_fn.go --with-expecter @@ -23,6 +25,7 @@ type ExportWorker struct { repositoryResources resources.RepositoryResourcesFactory exportFn ExportFn wrapWithStageFn WrapWithStageFn + metrics jobs.JobMetrics } func NewExportWorker( @@ -30,12 +33,14 @@ func NewExportWorker( repositoryResources resources.RepositoryResourcesFactory, exportFn ExportFn, wrapWithStageFn WrapWithStageFn, + metrics jobs.JobMetrics, ) *ExportWorker { return &ExportWorker{ clientFactory: clientFactory, repositoryResources: repositoryResources, exportFn: exportFn, wrapWithStageFn: wrapWithStageFn, + metrics: metrics, } } @@ -50,6 +55,13 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository, return errors.New("missing export settings") } + logger := logging.FromContext(ctx).With("job", job.GetName(), "namespace", job.GetNamespace()) + start := time.Now() + outcome := utils.ErrorOutcome + resourcesExported := 0 + defer func() { + r.metrics.RecordJob(string(provisioning.JobActionPush), outcome, resourcesExported, time.Since(start).Seconds()) + }() cfg := repo.Config() // Can write to external branch if err := repository.IsWriteAllowed(cfg, options.Branch); err != nil { @@ -72,16 +84,19 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository, fn := func(repo repository.Repository, _ bool) error { clients, err := r.clientFactory.Clients(ctx, cfg.Namespace) if err != nil { + logger.Error("failed to create clients", "error", err) return fmt.Errorf("create clients: %w", err) } rw, ok := repo.(repository.ReaderWriter) if !ok { + logger.Error("export job submitted targeting repository that is not a ReaderWriter") return errors.New("export job submitted targeting repository that is not a ReaderWriter") } repositoryResources, err := r.repositoryResources.Client(ctx, rw) if err != nil { + logger.Error("failed to create repository resource client", "error", err) return fmt.Errorf("create repository resource client: %w", err) } @@ -99,5 +114,16 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository, } } - return err + if err != nil { + logger.Error("failed to export", "error", err) + return err + } + + outcome = utils.SuccessOutcome + jobStatus := progress.Complete(ctx, nil) + for _, summary := range jobStatus.Summary { + resourcesExported += int(summary.Write) + } + + return nil } diff --git a/pkg/registry/apis/provisioning/jobs/export/worker_test.go b/pkg/registry/apis/provisioning/jobs/export/worker_test.go index 21cb19d0bda..c5bf32a62b6 100644 --- a/pkg/registry/apis/provisioning/jobs/export/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/export/worker_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus" mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -18,6 +19,7 @@ import ( ) func TestExportWorker_IsSupported(t *testing.T) { + metrics := jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()) tests := []struct { name string job v0alpha1.Job @@ -54,7 +56,7 @@ func TestExportWorker_IsSupported(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := NewExportWorker(nil, nil, nil, nil) + r := NewExportWorker(nil, nil, nil, nil, metrics) got := r.IsSupported(context.Background(), tt.job) require.Equal(t, tt.want, got) }) @@ -68,7 +70,7 @@ func TestExportWorker_ProcessNoExportSettings(t *testing.T) { }, } - r := NewExportWorker(nil, nil, nil, nil) + r := NewExportWorker(nil, nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), nil, job, nil) require.EqualError(t, err, "missing export settings") } @@ -91,7 +93,7 @@ func TestExportWorker_ProcessWriteNotAllowed(t *testing.T) { }, }) - r := NewExportWorker(nil, nil, nil, nil) + r := NewExportWorker(nil, nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepo, job, nil) require.EqualError(t, err, "this repository is read only") } @@ -115,7 +117,7 @@ func TestExportWorker_ProcessBranchNotAllowedForLocal(t *testing.T) { }, }) - r := NewExportWorker(nil, nil, nil, nil) + r := NewExportWorker(nil, nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepo, job, nil) require.EqualError(t, err, "this repository does not support the branch workflow") } @@ -147,7 +149,7 @@ func TestExportWorker_ProcessFailedToCreateClients(t *testing.T) { return fn(repo, true) }) - r := NewExportWorker(mockClients, nil, nil, mockStageFn.Execute) + r := NewExportWorker(mockClients, nil, nil, mockStageFn.Execute, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) mockProgress := jobs.NewMockJobProgressRecorder(t) err := r.Process(context.Background(), mockRepo, job, mockProgress) @@ -183,7 +185,7 @@ func TestExportWorker_ProcessNotReaderWriter(t *testing.T) { return fn(repo, true) }) - r := NewExportWorker(mockClients, nil, nil, mockStageFn.Execute) + r := NewExportWorker(mockClients, nil, nil, mockStageFn.Execute, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "export job submitted targeting repository that is not a ReaderWriter") } @@ -219,7 +221,7 @@ func TestExportWorker_ProcessRepositoryResourcesError(t *testing.T) { mockStageFn.On("Execute", context.Background(), mockRepo, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, nil, mockStageFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, nil, mockStageFn.Execute, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "create repository resource client: failed to create repository resources client") } @@ -247,6 +249,7 @@ func TestExportWorker_ProcessStageOptions(t *testing.T) { }) mockProgress := jobs.NewMockJobProgressRecorder(t) + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) // No progress messages expected in current implementation mockClients := resources.NewMockClientFactory(t) @@ -270,7 +273,7 @@ func TestExportWorker_ProcessStageOptions(t *testing.T) { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -330,6 +333,7 @@ func TestExportWorker_ProcessStageOptionsWithBranch(t *testing.T) { }) mockProgress := jobs.NewMockJobProgressRecorder(t) + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) mockClients := resources.NewMockClientFactory(t) mockResourceClients := resources.NewMockResourceClients(t) mockClients.On("Clients", mock.Anything, "test-namespace").Return(mockResourceClients, nil) @@ -351,7 +355,7 @@ func TestExportWorker_ProcessStageOptionsWithBranch(t *testing.T) { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) }) @@ -394,7 +398,7 @@ func TestExportWorker_ProcessExportFnError(t *testing.T) { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "export failed") } @@ -422,7 +426,7 @@ func TestExportWorker_ProcessWrapWithStageFnError(t *testing.T) { mockStageFn := NewMockWrapWithStageFn(t) mockStageFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything).Return(errors.New("stage failed")) - r := NewExportWorker(nil, nil, nil, mockStageFn.Execute) + r := NewExportWorker(nil, nil, nil, mockStageFn.Execute, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "stage failed") } @@ -448,7 +452,7 @@ func TestExportWorker_ProcessBranchNotAllowedForStageableRepositories(t *testing mockProgress := jobs.NewMockJobProgressRecorder(t) // No progress messages expected in current implementation - r := NewExportWorker(nil, nil, nil, nil) + r := NewExportWorker(nil, nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "this repository does not support the branch workflow") } @@ -478,6 +482,7 @@ func TestExportWorker_ProcessGitRepository(t *testing.T) { }) mockProgress := jobs.NewMockJobProgressRecorder(t) + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) // No progress messages expected in current implementation mockClients := resources.NewMockClientFactory(t) @@ -499,7 +504,7 @@ func TestExportWorker_ProcessGitRepository(t *testing.T) { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -545,7 +550,7 @@ func TestExportWorker_ProcessGitRepositoryExportFnError(t *testing.T) { return fn(repo, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "export failed") } @@ -586,7 +591,7 @@ func TestExportWorker_RefURLsSetWithBranch(t *testing.T) { // Mock progress recorder to expect SetRefURLs call mockProgress := jobs.NewMockJobProgressRecorder(t) mockProgress.On("SetRefURLs", mock.Anything, expectedRefURLs).Once() - + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) // Mock other dependencies mockClients := resources.NewMockClientFactory(t) mockResourceClients := resources.NewMockResourceClients(t) @@ -608,7 +613,7 @@ func TestExportWorker_RefURLsSetWithBranch(t *testing.T) { return fn(mockReaderWriter, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepoWithURLs, job, mockProgress) require.NoError(t, err) @@ -643,6 +648,7 @@ func TestExportWorker_RefURLsNotSetWithoutBranch(t *testing.T) { // Mock progress recorder - SetRefURLs should NOT be called mockProgress := jobs.NewMockJobProgressRecorder(t) + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) // Explicitly NOT expecting SetRefURLs call // Mock other dependencies @@ -664,7 +670,7 @@ func TestExportWorker_RefURLsNotSetWithoutBranch(t *testing.T) { return fn(mockReaderWriter, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepoWithURLs, job, mockProgress) require.NoError(t, err) @@ -699,6 +705,7 @@ func TestExportWorker_RefURLsNotSetForNonURLRepository(t *testing.T) { // Mock progress recorder - SetRefURLs should NOT be called mockProgress := jobs.NewMockJobProgressRecorder(t) + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(v0alpha1.JobStatus{}) // Explicitly NOT expecting SetRefURLs call // Mock other dependencies @@ -720,7 +727,7 @@ func TestExportWorker_RefURLsNotSetForNonURLRepository(t *testing.T) { return fn(mockReaderWriter, true) }) - r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := r.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) diff --git a/pkg/registry/apis/provisioning/jobs/metrics.go b/pkg/registry/apis/provisioning/jobs/metrics.go new file mode 100644 index 00000000000..dc946838659 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/metrics.go @@ -0,0 +1,103 @@ +package jobs + +import ( + "github.com/grafana/grafana/pkg/registry/apis/provisioning/utils" + "github.com/prometheus/client_golang/prometheus" +) + +type JobMetrics struct { + registry prometheus.Registerer + processedTotal *prometheus.CounterVec + durationHist *prometheus.HistogramVec +} + +type QueueMetrics struct { + queueSize *prometheus.GaugeVec + queueWaitTime *prometheus.HistogramVec +} + +func RegisterQueueMetrics(registry prometheus.Registerer) QueueMetrics { + queueSize := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "grafana_provisioning_jobs_queue_size", + Help: "Number of jobs currently in the queue", + }, + []string{"action"}, + ) + registry.MustRegister(queueSize) + + queueWaitTime := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "grafana_provisioning_jobs_queue_wait_seconds", + Help: "Time jobs spend waiting in the queue before being claimed", + Buckets: []float64{1.0, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0}, + }, + []string{"action"}, + ) + registry.MustRegister(queueWaitTime) + + return QueueMetrics{ + queueSize: queueSize, + queueWaitTime: queueWaitTime, + } +} + +func (m *QueueMetrics) IncreaseQueueSize(action string) { + m.queueSize.WithLabelValues(action).Inc() +} + +func (m *QueueMetrics) DecreaseQueueSize(action string) { + m.queueSize.WithLabelValues(action).Dec() +} + +func (m *QueueMetrics) RecordWaitTime(action string, waitSeconds float64) { + m.queueWaitTime.WithLabelValues(action).Observe(waitSeconds) +} + +func RegisterJobMetrics(registry prometheus.Registerer) JobMetrics { + processedTotal := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "grafana_provisioning_jobs_processed_total", + Help: "Total number of jobs processed", + }, + []string{"action", "outcome"}, + ) + registry.MustRegister(processedTotal) + + durationHist := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "grafana_provisioning_jobs_duration_seconds", + Help: "Duration of job", + Buckets: []float64{5.0, 10.0, 30.0, 60.0, 120.0, 300.0}, + }, + []string{"action", "resources_changed_bucket"}, + ) + registry.MustRegister(durationHist) + + return JobMetrics{ + registry: registry, + processedTotal: processedTotal, + durationHist: durationHist, + } +} + +func (m *JobMetrics) RecordJob(jobAction string, outcome string, resourceCountChanged int, duration float64) { + m.processedTotal.WithLabelValues(jobAction, outcome).Inc() + + // only record duration when the job was successful. otherwise resource count will be incorrect + if outcome == utils.SuccessOutcome { + m.durationHist.WithLabelValues(jobAction, utils.GetResourceCountBucket(resourceCountChanged)).Observe(duration) + } +} + +func recordConcurrentDriverMetric(registry prometheus.Registerer, numDrivers int) { + concurrentDriver := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "grafana_provisioning_jobs_concurrent_driver_num_drivers", + Help: "Number of concurrent job drivers", + }, + []string{}, + ) + registry.MustRegister(concurrentDriver) + concurrentDriver.WithLabelValues().Set(float64(numDrivers)) +} diff --git a/pkg/registry/apis/provisioning/jobs/move/worker.go b/pkg/registry/apis/provisioning/jobs/move/worker.go index 790630f101e..1ec3d817b3e 100644 --- a/pkg/registry/apis/provisioning/jobs/move/worker.go +++ b/pkg/registry/apis/provisioning/jobs/move/worker.go @@ -9,24 +9,28 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/grafana/grafana-app-sdk/logging" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/apps/provisioning/pkg/safepath" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/utils" ) type Worker struct { syncWorker jobs.Worker wrapFn repository.WrapWithStageFn resourcesFactory resources.RepositoryResourcesFactory + metrics jobs.JobMetrics } -func NewWorker(syncWorker jobs.Worker, wrapFn repository.WrapWithStageFn, resourcesFactory resources.RepositoryResourcesFactory) *Worker { +func NewWorker(syncWorker jobs.Worker, wrapFn repository.WrapWithStageFn, resourcesFactory resources.RepositoryResourcesFactory, metrics jobs.JobMetrics) *Worker { return &Worker{ syncWorker: syncWorker, wrapFn: wrapFn, resourcesFactory: resourcesFactory, + metrics: metrics, } } @@ -39,6 +43,13 @@ func (w *Worker) Process(ctx context.Context, repo repository.Repository, job pr return errors.New("missing move settings") } opts := *job.Spec.Move + logger := logging.FromContext(ctx).With("job", job.GetName(), "namespace", job.GetNamespace()) + outcome := utils.ErrorOutcome + start := time.Now() + resourcesMoved := 0 + defer func() { + w.metrics.RecordJob(string(provisioning.JobActionMove), outcome, resourcesMoved, time.Since(start).Seconds()) + }() if opts.TargetPath == "" { return errors.New("target path is required for move operation") @@ -57,6 +68,7 @@ func (w *Worker) Process(ctx context.Context, repo repository.Repository, job pr fn := func(repo repository.Repository, _ bool) error { rw, ok := repo.(repository.ReaderWriter) if !ok { + logger.Error("move job submitted targeting repository that is not a ReaderWriter") return errors.New("move job submitted targeting repository that is not a ReaderWriter") } @@ -86,6 +98,7 @@ func (w *Worker) Process(ctx context.Context, repo repository.Repository, job pr err := w.wrapFn(ctx, repo, stageOptions, fn) if err != nil { + logger.Error("failed to move files in repository", "error", err) return fmt.Errorf("move files in repository: %w", err) } @@ -112,10 +125,18 @@ func (w *Worker) Process(ctx context.Context, repo repository.Repository, job pr } if err := w.syncWorker.Process(ctx, repo, syncJob, progress); err != nil { + logger.Error("failed to pull resources", "error", err) return fmt.Errorf("pull resources: %w", err) } } + outcome = utils.SuccessOutcome + jobStatus := progress.Complete(ctx, nil) + for _, summary := range jobStatus.Summary { + // FileActionRenamed increments both delete & create, use create here + resourcesMoved += int(summary.Create) + } + return nil } diff --git a/pkg/registry/apis/provisioning/jobs/move/worker_test.go b/pkg/registry/apis/provisioning/jobs/move/worker_test.go index 3f9ea095d60..f5129350e6e 100644 --- a/pkg/registry/apis/provisioning/jobs/move/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/move/worker_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/apps/provisioning/pkg/safepath" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -30,6 +31,7 @@ func (m *mockReaderWriter) Move(ctx context.Context, oldPath, newPath, ref, mess } func TestMoveWorker_IsSupported(t *testing.T) { + metrics := jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()) tests := []struct { name string job provisioning.Job @@ -75,7 +77,7 @@ func TestMoveWorker_IsSupported(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - worker := NewWorker(nil, nil, nil) + worker := NewWorker(nil, nil, nil, metrics) result := worker.IsSupported(context.Background(), tt.job) require.Equal(t, tt.expected, result) }) @@ -89,7 +91,7 @@ func TestMoveWorker_ProcessMissingMoveSettings(t *testing.T) { }, } - worker := NewWorker(nil, nil, nil) + worker := NewWorker(nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), nil, job, nil) require.EqualError(t, err, "missing move settings") } @@ -104,7 +106,7 @@ func TestMoveWorker_ProcessMissingTargetPath(t *testing.T) { }, } - worker := NewWorker(nil, nil, nil) + worker := NewWorker(nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), nil, job, nil) require.EqualError(t, err, "target path is required for move operation") } @@ -120,7 +122,7 @@ func TestMoveWorker_ProcessInvalidTargetPath(t *testing.T) { }, } - worker := NewWorker(nil, nil, nil) + worker := NewWorker(nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), nil, job, nil) require.EqualError(t, err, "target path must be a directory (should end with '/')") } @@ -152,7 +154,7 @@ func TestMoveWorker_ProcessNotReaderWriter(t *testing.T) { mockProgress.On("SetTotal", mock.Anything, 1).Return() mockProgress.On("StrictMaxErrors", 1).Return() - worker := NewWorker(nil, mockWrapFn.Execute, nil) + worker := NewWorker(nil, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "move files in repository: move job submitted targeting repository that is not a ReaderWriter") } @@ -176,7 +178,7 @@ func TestMoveWorker_ProcessWrapFnError(t *testing.T) { mockProgress.On("SetTotal", mock.Anything, 1).Return() mockProgress.On("StrictMaxErrors", 1).Return() - worker := NewWorker(nil, mockWrapFn.Execute, nil) + worker := NewWorker(nil, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "move files in repository: stage failed") } @@ -212,6 +214,7 @@ func TestMoveWorker_ProcessMoveFilesSuccess(t *testing.T) { mockProgress.On("SetMessage", mock.Anything, "Moving test/path1 to new/location/path1").Return() mockProgress.On("SetMessage", mock.Anything, "Moving test/path2 to new/location/path2").Return() mockProgress.On("TooManyErrors").Return(nil).Twice() + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(provisioning.JobStatus{}) mockRepo.On("Move", mock.Anything, "test/path1", "new/location/path1", "main", "Move test/path1 to new/location/path1").Return(nil) mockRepo.On("Move", mock.Anything, "test/path2", "new/location/path2", "main", "Move test/path2 to new/location/path2").Return(nil) @@ -223,7 +226,7 @@ func TestMoveWorker_ProcessMoveFilesSuccess(t *testing.T) { return result.Path == "test/path2" && result.Action == repository.FileActionRenamed && result.Error == nil })).Return() - worker := NewWorker(nil, mockWrapFn.Execute, nil) + worker := NewWorker(nil, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -262,7 +265,7 @@ func TestMoveWorker_ProcessMoveFilesWithError(t *testing.T) { })).Return() mockProgress.On("TooManyErrors").Return(errors.New("too many errors")) - worker := NewWorker(nil, mockWrapFn.Execute, nil) + worker := NewWorker(nil, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "move files in repository: too many errors") } @@ -302,12 +305,13 @@ func TestMoveWorker_ProcessWithSyncWorker(t *testing.T) { mockProgress.On("ResetResults").Return() mockProgress.On("SetMessage", mock.Anything, "pull resources").Return() + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(provisioning.JobStatus{}) mockSyncWorker.On("Process", mock.Anything, mockRepo, mock.MatchedBy(func(syncJob provisioning.Job) bool { return syncJob.Spec.Pull != nil && !syncJob.Spec.Pull.Incremental }), mockProgress).Return(nil) - worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil) + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -348,7 +352,7 @@ func TestMoveWorker_ProcessSyncWorkerError(t *testing.T) { syncError := errors.New("sync failed") mockSyncWorker.On("Process", mock.Anything, mockRepo, mock.Anything, mockProgress).Return(syncError) - worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil) + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "pull resources: sync failed") } @@ -429,7 +433,7 @@ func TestMoveWorker_moveFiles(t *testing.T) { } } - worker := NewWorker(nil, nil, nil) + worker := NewWorker(nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.moveFiles(context.Background(), mockRepo, mockProgress, opts, tt.paths...) if tt.expectedError != "" { @@ -485,7 +489,7 @@ func TestMoveWorker_constructTargetPath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - worker := NewWorker(nil, nil, nil) + worker := NewWorker(nil, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) result := worker.constructTargetPath(tt.jobTargetPath, tt.sourcePath) require.Equal(t, tt.expectedTarget, result) }) @@ -530,7 +534,7 @@ func TestMoveWorker_ProcessWithResourceReferences(t *testing.T) { mockProgress.On("SetMessage", mock.Anything, "Moving test/path1 to new/location/path1").Return() mockProgress.On("SetMessage", mock.Anything, "Moving dashboard/file.yaml to new/location/file.yaml").Return() mockProgress.On("TooManyErrors").Return(nil).Times(2) - + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(provisioning.JobStatus{}) mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepoResources, nil) mockRepoResources.On("FindResourcePath", mock.Anything, "dashboard-uid", schema.GroupVersionKind{ Group: "dashboard.grafana.app", @@ -554,7 +558,7 @@ func TestMoveWorker_ProcessWithResourceReferences(t *testing.T) { return syncJob.Spec.Pull != nil && !syncJob.Spec.Pull.Incremental }), mockProgress).Return(nil) - worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) } @@ -593,6 +597,7 @@ func TestMoveWorker_ProcessResourceReferencesError(t *testing.T) { mockProgress.On("SetMessage", mock.Anything, "Resolving resource paths").Return() mockProgress.On("SetMessage", mock.Anything, "Finding path for resource dashboard.grafana.app/Dashboard/non-existent-uid").Return() mockProgress.On("TooManyErrors").Return(nil) + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(provisioning.JobStatus{}) mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(mockRepoResources, nil) resourceError := errors.New("resource not found") @@ -615,7 +620,7 @@ func TestMoveWorker_ProcessResourceReferencesError(t *testing.T) { return syncJob.Spec.Pull != nil && !syncJob.Spec.Pull.Incremental }), mockProgress).Return(nil) - worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) // Should continue despite individual resource errors } @@ -655,7 +660,7 @@ func TestMoveWorker_ProcessResourcesFactoryError(t *testing.T) { factoryError := errors.New("failed to create resources client") mockResourcesFactory.On("Client", mock.Anything, mockRepo).Return(nil, factoryError) - worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.EqualError(t, err, "move files in repository: create repository resources client: failed to create resources client") } @@ -773,7 +778,7 @@ func TestMoveWorker_resolveResourcesToPaths(t *testing.T) { } } - worker := NewWorker(nil, nil, mockResourcesFactory) + worker := NewWorker(nil, nil, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) paths, err := worker.resolveResourcesToPaths(context.Background(), mockRepo, mockProgress, tt.resources) if tt.expectedError != "" { @@ -862,7 +867,7 @@ func TestMoveWorker_RefURLsSetWithRef(t *testing.T) { mockProgress.On("Record", mock.Anything, mock.Anything).Once() mockProgress.On("TooManyErrors").Return(nil).Once() mockProgress.On("SetRefURLs", mock.Anything, expectedRefURLs).Once() - + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(provisioning.JobStatus{}) mockReaderWriter := repository.NewMockReaderWriter(t) mockReaderWriter.On("Move", mock.Anything, "test.json", "target/test.json", "feature-branch", "Move test.json to target/test.json").Return(nil) @@ -885,7 +890,7 @@ func TestMoveWorker_RefURLsSetWithRef(t *testing.T) { }, } - worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepoWithURLs, job, mockProgress) require.NoError(t, err) @@ -915,6 +920,7 @@ func TestMoveWorker_RefURLsNotSetWithoutRef(t *testing.T) { mockProgress.On("TooManyErrors").Return(nil).Once() mockProgress.On("ResetResults").Once() mockProgress.On("SetMessage", mock.Anything, "pull resources").Once() + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(provisioning.JobStatus{}) // SetRefURLs should NOT be called since no ref is specified mockReaderWriter := repository.NewMockReaderWriter(t) @@ -942,7 +948,7 @@ func TestMoveWorker_RefURLsNotSetWithoutRef(t *testing.T) { }, } - worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(mockSyncWorker, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepoWithURLs, job, mockProgress) require.NoError(t, err) @@ -969,6 +975,7 @@ func TestMoveWorker_RefURLsNotSetForNonURLRepository(t *testing.T) { mockProgress.On("SetMessage", mock.Anything, "Moving test.json to target/test.json").Once() mockProgress.On("Record", mock.Anything, mock.Anything).Once() mockProgress.On("TooManyErrors").Return(nil).Once() + mockProgress.On("Complete", mock.Anything, mock.Anything).Return(provisioning.JobStatus{}) // SetRefURLs should NOT be called since repo doesn't support URLs mockReaderWriter := repository.NewMockReaderWriter(t) @@ -993,7 +1000,7 @@ func TestMoveWorker_RefURLsNotSetForNonURLRepository(t *testing.T) { }, } - worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory) + worker := NewWorker(nil, mockWrapFn.Execute, mockResourcesFactory, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry())) err := worker.Process(context.Background(), mockRepo, job, mockProgress) require.NoError(t, err) diff --git a/pkg/registry/apis/provisioning/jobs/persistentstore.go b/pkg/registry/apis/provisioning/jobs/persistentstore.go index 5604e7bff0c..78494aa82cc 100644 --- a/pkg/registry/apis/provisioning/jobs/persistentstore.go +++ b/pkg/registry/apis/provisioning/jobs/persistentstore.go @@ -18,6 +18,7 @@ import ( provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" client "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/prometheus/client_golang/prometheus" ) const ( @@ -72,18 +73,23 @@ type persistentStore struct { // expiry is the time after which a job is considered abandoned. // If a job is abandoned, it will have its claim cleaned up periodically. expiry time.Duration + + queueMetrics QueueMetrics } // NewJobStore creates a new job queue implementation using the API client. -func NewJobStore(provisioningClient client.ProvisioningV0alpha1Interface, expiry time.Duration) (*persistentStore, error) { +func NewJobStore(provisioningClient client.ProvisioningV0alpha1Interface, expiry time.Duration, registry prometheus.Registerer) (*persistentStore, error) { if expiry <= 0 { expiry = time.Second * 30 } + queueMetrics := RegisterQueueMetrics(registry) + return &persistentStore{ - client: provisioningClient, - clock: time.Now, - expiry: expiry, + client: provisioningClient, + clock: time.Now, + expiry: expiry, + queueMetrics: queueMetrics, }, nil } @@ -116,6 +122,7 @@ func (s *persistentStore) Claim(ctx context.Context) (job *provisioning.Job, rol job.Labels = make(map[string]string) } job.Labels[LabelJobClaim] = strconv.FormatInt(s.clock().UnixMilli(), 10) + s.queueMetrics.RecordWaitTime(string(job.Spec.Action), s.clock().Sub(job.CreationTimestamp.Time).Seconds()) // Set up the provisioning identity for this namespace ctx, _, err = identity.WithProvisioningIdentity(ctx, job.GetNamespace()) @@ -237,6 +244,7 @@ func (s *persistentStore) Complete(ctx context.Context, job *provisioning.Job) e job.Labels = make(map[string]string) } delete(job.Labels, LabelJobClaim) + s.queueMetrics.DecreaseQueueSize(string(job.Spec.Action)) logger.Debug("job completion done") return nil @@ -379,6 +387,8 @@ func (s *persistentStore) Insert(ctx context.Context, namespace string, spec pro return nil, apifmt.Errorf("failed to create job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err) } + s.queueMetrics.IncreaseQueueSize(string(job.Spec.Action)) + return created, nil } diff --git a/pkg/registry/apis/provisioning/jobs/sync/full.go b/pkg/registry/apis/provisioning/jobs/sync/full.go index 8114d8a67ce..0e0b107171a 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/full.go +++ b/pkg/registry/apis/provisioning/jobs/sync/full.go @@ -6,8 +6,11 @@ import ( "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/apps/provisioning/pkg/safepath" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -20,47 +23,63 @@ func FullSync( currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, + tracer tracing.Tracer, ) error { cfg := repo.Config() + ctx, span := tracer.Start(ctx, "provisioning.sync.full") + defer span.End() + + ensureFolderCtx, ensureFolderSpan := tracer.Start(ctx, "provisioning.sync.full.ensure_folder_exists") // Ensure the configured folder exists and is managed by the repository rootFolder := resources.RootFolder(cfg) if rootFolder != "" { - if err := repositoryResources.EnsureFolderExists(ctx, resources.Folder{ + if err := repositoryResources.EnsureFolderExists(ensureFolderCtx, resources.Folder{ ID: rootFolder, // will not change if exists Title: cfg.Spec.Title, Path: "", // at the root of the repository }, ""); err != nil { - return fmt.Errorf("create root folder: %w", err) + ensureFolderSpan.End() + return tracing.Error(span, fmt.Errorf("create root folder: %w", err)) } } + ensureFolderSpan.End() - changes, err := compare(ctx, repo, repositoryResources, currentRef) + compareCtx, compareSpan := tracer.Start(ctx, "provisioning.sync.full.compare") + changes, err := compare(compareCtx, repo, repositoryResources, currentRef) if err != nil { - return fmt.Errorf("compare changes: %w", err) + compareSpan.End() + return tracing.Error(span, fmt.Errorf("compare changes: %w", err)) } + compareSpan.End() if len(changes) == 0 { progress.SetFinalMessage(ctx, "no changes to sync") return nil } - return applyChanges(ctx, changes, clients, repositoryResources, progress) + return applyChanges(ctx, changes, clients, repositoryResources, progress, tracer) } -func applyChanges(ctx context.Context, changes []ResourceFileChange, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error { +func applyChanges(ctx context.Context, changes []ResourceFileChange, clients resources.ResourceClients, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer) error { progress.SetTotal(ctx, len(changes)) + _, applyChangesSpan := tracer.Start(ctx, "provisioning.sync.full.apply_changes", + trace.WithAttributes(attribute.Int("changes_count", len(changes))), + ) + defer applyChangesSpan.End() + for _, change := range changes { if ctx.Err() != nil { return ctx.Err() } if err := progress.TooManyErrors(); err != nil { - return err + return tracing.Error(applyChangesSpan, err) } if change.Action == repository.FileActionDeleted { + deleteCtx, deleteSpan := tracer.Start(ctx, "provisioning.sync.full.apply_changes.delete") result := jobs.JobResourceResult{ Path: change.Path, Action: change.Action, @@ -68,7 +87,9 @@ func applyChanges(ctx context.Context, changes []ResourceFileChange, clients res if change.Existing == nil || change.Existing.Name == "" { result.Error = fmt.Errorf("processing deletion for file %s: missing existing reference", change.Path) - progress.Record(ctx, result) + progress.Record(deleteCtx, result) + deleteSpan.RecordError(result.Error) + deleteSpan.End() continue } @@ -82,22 +103,24 @@ func applyChanges(ctx context.Context, changes []ResourceFileChange, clients res } // TODO: should we use the clients or the resource manager instead? - client, _, err := clients.ForResource(ctx, versionlessGVR) + client, _, err := clients.ForResource(deleteCtx, versionlessGVR) if err != nil { result.Error = fmt.Errorf("get client for deleted object: %w", err) - progress.Record(ctx, result) + progress.Record(deleteCtx, result) continue } - if err := client.Delete(ctx, change.Existing.Name, metav1.DeleteOptions{}); err != nil { + if err := client.Delete(deleteCtx, change.Existing.Name, metav1.DeleteOptions{}); err != nil { result.Error = fmt.Errorf("deleting resource %s/%s %s: %w", change.Existing.Group, change.Existing.Resource, change.Existing.Name, err) } - progress.Record(ctx, result) + progress.Record(deleteCtx, result) + deleteSpan.End() continue } // If folder ensure it exists if safepath.IsDir(change.Path) { + ensureFolderCtx, ensureFolderSpan := tracer.Start(ctx, "provisioning.sync.full.apply_changes.ensure_folder_exists") result := jobs.JobResourceResult{ Path: change.Path, Action: change.Action, @@ -105,20 +128,24 @@ func applyChanges(ctx context.Context, changes []ResourceFileChange, clients res Group: resources.FolderResource.Group, } - folder, err := repositoryResources.EnsureFolderPathExist(ctx, change.Path) + folder, err := repositoryResources.EnsureFolderPathExist(ensureFolderCtx, change.Path) if err != nil { result.Error = fmt.Errorf("ensuring folder exists at path %s: %w", change.Path, err) + ensureFolderSpan.RecordError(err) + ensureFolderSpan.End() progress.Record(ctx, result) continue } result.Name = folder - progress.Record(ctx, result) + progress.Record(ensureFolderCtx, result) + ensureFolderSpan.End() continue } - name, gvk, err := repositoryResources.WriteResourceFromFile(ctx, change.Path, "") + writeCtx, writeSpan := tracer.Start(ctx, "provisioning.sync.full.apply_changes.write_resource_from_file") + name, gvk, err := repositoryResources.WriteResourceFromFile(writeCtx, change.Path, "") result := jobs.JobResourceResult{ Path: change.Path, Action: change.Action, @@ -128,9 +155,11 @@ func applyChanges(ctx context.Context, changes []ResourceFileChange, clients res } if err != nil { + writeSpan.RecordError(err) result.Error = fmt.Errorf("writing resource from file %s: %w", change.Path, err) } - progress.Record(ctx, result) + progress.Record(writeCtx, result) + writeSpan.End() } return nil diff --git a/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go b/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go index 03b080cff25..3f10550e3f8 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/full_sync_fn_mock.go @@ -11,6 +11,8 @@ import ( repository "github.com/grafana/grafana/apps/provisioning/pkg/repository" resources "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + + tracing "github.com/grafana/grafana/pkg/infra/tracing" ) // MockFullSyncFn is an autogenerated mock type for the FullSyncFn type @@ -26,17 +28,17 @@ func (_m *MockFullSyncFn) EXPECT() *MockFullSyncFn_Expecter { return &MockFullSyncFn_Expecter{mock: &_m.Mock} } -// Execute provides a mock function with given fields: ctx, repo, compare, clients, currentRef, repositoryResources, progress -func (_m *MockFullSyncFn) Execute(ctx context.Context, repo repository.Reader, compare CompareFn, clients resources.ResourceClients, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error { - ret := _m.Called(ctx, repo, compare, clients, currentRef, repositoryResources, progress) +// Execute provides a mock function with given fields: ctx, repo, compare, clients, currentRef, repositoryResources, progress, tracer +func (_m *MockFullSyncFn) Execute(ctx context.Context, repo repository.Reader, compare CompareFn, clients resources.ResourceClients, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer) error { + ret := _m.Called(ctx, repo, compare, clients, currentRef, repositoryResources, progress, tracer) if len(ret) == 0 { panic("no return value specified for Execute") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, repository.Reader, CompareFn, resources.ResourceClients, string, resources.RepositoryResources, jobs.JobProgressRecorder) error); ok { - r0 = rf(ctx, repo, compare, clients, currentRef, repositoryResources, progress) + if rf, ok := ret.Get(0).(func(context.Context, repository.Reader, CompareFn, resources.ResourceClients, string, resources.RepositoryResources, jobs.JobProgressRecorder, tracing.Tracer) error); ok { + r0 = rf(ctx, repo, compare, clients, currentRef, repositoryResources, progress, tracer) } else { r0 = ret.Error(0) } @@ -57,13 +59,14 @@ type MockFullSyncFn_Execute_Call struct { // - currentRef string // - repositoryResources resources.RepositoryResources // - progress jobs.JobProgressRecorder -func (_e *MockFullSyncFn_Expecter) Execute(ctx interface{}, repo interface{}, compare interface{}, clients interface{}, currentRef interface{}, repositoryResources interface{}, progress interface{}) *MockFullSyncFn_Execute_Call { - return &MockFullSyncFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, compare, clients, currentRef, repositoryResources, progress)} +// - tracer tracing.Tracer +func (_e *MockFullSyncFn_Expecter) Execute(ctx interface{}, repo interface{}, compare interface{}, clients interface{}, currentRef interface{}, repositoryResources interface{}, progress interface{}, tracer interface{}) *MockFullSyncFn_Execute_Call { + return &MockFullSyncFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, compare, clients, currentRef, repositoryResources, progress, tracer)} } -func (_c *MockFullSyncFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Reader, compare CompareFn, clients resources.ResourceClients, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder)) *MockFullSyncFn_Execute_Call { +func (_c *MockFullSyncFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Reader, compare CompareFn, clients resources.ResourceClients, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer)) *MockFullSyncFn_Execute_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(repository.Reader), args[2].(CompareFn), args[3].(resources.ResourceClients), args[4].(string), args[5].(resources.RepositoryResources), args[6].(jobs.JobProgressRecorder)) + run(args[0].(context.Context), args[1].(repository.Reader), args[2].(CompareFn), args[3].(resources.ResourceClients), args[4].(string), args[5].(resources.RepositoryResources), args[6].(jobs.JobProgressRecorder), args[7].(tracing.Tracer)) }) return _c } @@ -73,7 +76,7 @@ func (_c *MockFullSyncFn_Execute_Call) Return(_a0 error) *MockFullSyncFn_Execute return _c } -func (_c *MockFullSyncFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Reader, CompareFn, resources.ResourceClients, string, resources.RepositoryResources, jobs.JobProgressRecorder) error) *MockFullSyncFn_Execute_Call { +func (_c *MockFullSyncFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Reader, CompareFn, resources.ResourceClients, string, resources.RepositoryResources, jobs.JobProgressRecorder, tracing.Tracer) error) *MockFullSyncFn_Execute_Call { _c.Call.Return(run) return _c } diff --git a/pkg/registry/apis/provisioning/jobs/sync/full_test.go b/pkg/registry/apis/provisioning/jobs/sync/full_test.go index 0f051e20b72..d5b3b610b80 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/full_test.go +++ b/pkg/registry/apis/provisioning/jobs/sync/full_test.go @@ -8,6 +8,7 @@ import ( provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/stretchr/testify/mock" @@ -41,7 +42,7 @@ func TestFullSync_ContextCancelled(t *testing.T) { compareFn.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]ResourceFileChange{{}}, nil) progress.On("SetTotal", mock.Anything, 1).Return() - err := FullSync(ctx, repo, compareFn.Execute, clients, "current-ref", repoResources, progress) + err := FullSync(ctx, repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService()) require.EqualError(t, err, "context canceled") } @@ -60,7 +61,7 @@ func TestFullSync_Error(t *testing.T) { compareFn.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("some error")) - err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress) + err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService()) require.EqualError(t, err, "compare changes: some error") } @@ -80,7 +81,7 @@ func TestFullSync_NoChanges(t *testing.T) { compareFn.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]ResourceFileChange{}, nil) progress.On("SetFinalMessage", mock.Anything, "no changes to sync").Return() - err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress) + err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService()) require.NoError(t, err) } @@ -111,7 +112,7 @@ func TestFullSync_SuccessfulFolderCreation(t *testing.T) { Path: "", }, "").Return(nil) - err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress) + err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService()) require.NoError(t, err) } @@ -140,7 +141,7 @@ func TestFullSync_FolderCreationFailed(t *testing.T) { Path: "", }, "").Return(fmt.Errorf("folder creation failed")) - err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress) + err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService()) require.Error(t, err) require.Contains(t, err.Error(), "create root folder: folder creation failed") } @@ -169,7 +170,7 @@ func TestFullSync_FolderCreationFailedWithInstanceTarget(t *testing.T) { compareFn.On("Execute", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(nil, fmt.Errorf("compare error")) - err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress) + err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService()) require.Error(t, err) require.Contains(t, err.Error(), "compare changes: compare error") } @@ -700,7 +701,7 @@ func TestFullSync_ApplyChanges(t *testing.T) { //nolint:gocyclo }) progress.On("SetTotal", mock.Anything, len(tt.changes)).Return() - err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress) + err := FullSync(context.Background(), repo, compareFn.Execute, clients, "current-ref", repoResources, progress, tracing.NewNoopTracerService()) if tt.expectedError != "" { require.EqualError(t, err, tt.expectedError, tt.description) } else { diff --git a/pkg/registry/apis/provisioning/jobs/sync/incremental.go b/pkg/registry/apis/provisioning/jobs/sync/incremental.go index fe66ad058ee..f7448bffa16 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/incremental.go +++ b/pkg/registry/apis/provisioning/jobs/sync/incremental.go @@ -6,21 +6,29 @@ import ( "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/apps/provisioning/pkg/safepath" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" ) // Convert git changes into resource file changes -func IncrementalSync(ctx context.Context, repo repository.Versioned, previousRef, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error { +func IncrementalSync(ctx context.Context, repo repository.Versioned, previousRef, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer) error { if previousRef == currentRef { progress.SetFinalMessage(ctx, "same commit as last time") return nil } - diff, err := repo.CompareFiles(ctx, previousRef, currentRef) + ctx, span := tracer.Start(ctx, "provisioning.sync.incremental") + defer span.End() + + compareCtx, compareSpan := tracer.Start(ctx, "provisioning.sync.incremental.compare_files") + diff, err := repo.CompareFiles(compareCtx, previousRef, currentRef) if err != nil { - return fmt.Errorf("compare files error: %w", err) + compareSpan.RecordError(err) + compareSpan.End() + return tracing.Error(span, fmt.Errorf("compare files error: %w", err)) } + compareSpan.End() if len(diff) < 1 { progress.SetFinalMessage(ctx, "no changes detected between commits") @@ -35,10 +43,11 @@ func IncrementalSync(ctx context.Context, repo repository.Versioned, previousRef return ctx.Err() } if err := progress.TooManyErrors(); err != nil { - return err + return tracing.Error(span, err) } if err := resources.IsPathSupported(change.Path); err != nil { + ensureFolderCtx, ensureFolderSpan := tracer.Start(ctx, "provisioning.sync.incremental.ensure_folder_path_exist") // Maintain the safe segment for empty folders safeSegment := safepath.SafeSegment(change.Path) if !safepath.IsDir(safeSegment) { @@ -46,26 +55,29 @@ func IncrementalSync(ctx context.Context, repo repository.Versioned, previousRef } if safeSegment != "" && resources.IsPathSupported(safeSegment) == nil { - folder, err := repositoryResources.EnsureFolderPathExist(ctx, safeSegment) + folder, err := repositoryResources.EnsureFolderPathExist(ensureFolderCtx, safeSegment) if err != nil { - return fmt.Errorf("unable to create empty file folder: %w", err) + ensureFolderSpan.RecordError(err) + ensureFolderSpan.End() + return tracing.Error(span, fmt.Errorf("unable to create empty file folder: %w", err)) } - progress.Record(ctx, jobs.JobResourceResult{ + progress.Record(ensureFolderCtx, jobs.JobResourceResult{ Path: safeSegment, Action: repository.FileActionCreated, Resource: resources.FolderResource.Resource, Group: resources.FolderResource.Group, Name: folder, }) - + ensureFolderSpan.End() continue } - progress.Record(ctx, jobs.JobResourceResult{ + progress.Record(ensureFolderCtx, jobs.JobResourceResult{ Path: change.Path, Action: repository.FileActionIgnored, }) + ensureFolderSpan.End() continue } @@ -76,29 +88,38 @@ func IncrementalSync(ctx context.Context, repo repository.Versioned, previousRef switch change.Action { case repository.FileActionCreated, repository.FileActionUpdated: - name, gvk, err := repositoryResources.WriteResourceFromFile(ctx, change.Path, change.Ref) + writeCtx, writeSpan := tracer.Start(ctx, "provisioning.sync.incremental.write_resource_from_file") + name, gvk, err := repositoryResources.WriteResourceFromFile(writeCtx, change.Path, change.Ref) if err != nil { + writeSpan.RecordError(err) result.Error = fmt.Errorf("writing resource from file %s: %w", change.Path, err) } result.Name = name result.Resource = gvk.Kind result.Group = gvk.Group + writeSpan.End() case repository.FileActionDeleted: - name, gvk, err := repositoryResources.RemoveResourceFromFile(ctx, change.Path, change.PreviousRef) + removeCtx, removeSpan := tracer.Start(ctx, "provisioning.sync.incremental.remove_resource_from_file") + name, gvk, err := repositoryResources.RemoveResourceFromFile(removeCtx, change.Path, change.PreviousRef) if err != nil { + removeSpan.RecordError(err) result.Error = fmt.Errorf("removing resource from file %s: %w", change.Path, err) } result.Name = name result.Resource = gvk.Kind result.Group = gvk.Group + removeSpan.End() case repository.FileActionRenamed: - name, gvk, err := repositoryResources.RenameResourceFile(ctx, change.PreviousPath, change.PreviousRef, change.Path, change.Ref) + renameCtx, renameSpan := tracer.Start(ctx, "provisioning.sync.incremental.rename_resource_file") + name, gvk, err := repositoryResources.RenameResourceFile(renameCtx, change.PreviousPath, change.PreviousRef, change.Path, change.Ref) if err != nil { + renameSpan.RecordError(err) result.Error = fmt.Errorf("renaming resource file from %s to %s: %w", change.PreviousPath, change.Path, err) } result.Name = name result.Resource = gvk.Kind result.Group = gvk.Group + renameSpan.End() case repository.FileActionIgnored: // do nothing } diff --git a/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go b/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go index c1ae1325475..ff032146788 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/sync/incremental_sync_fn_mock.go @@ -11,6 +11,8 @@ import ( repository "github.com/grafana/grafana/apps/provisioning/pkg/repository" resources "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + + tracing "github.com/grafana/grafana/pkg/infra/tracing" ) // MockIncrementalSyncFn is an autogenerated mock type for the IncrementalSyncFn type @@ -26,17 +28,17 @@ func (_m *MockIncrementalSyncFn) EXPECT() *MockIncrementalSyncFn_Expecter { return &MockIncrementalSyncFn_Expecter{mock: &_m.Mock} } -// Execute provides a mock function with given fields: ctx, repo, previousRef, currentRef, repositoryResources, progress -func (_m *MockIncrementalSyncFn) Execute(ctx context.Context, repo repository.Versioned, previousRef string, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error { - ret := _m.Called(ctx, repo, previousRef, currentRef, repositoryResources, progress) +// Execute provides a mock function with given fields: ctx, repo, previousRef, currentRef, repositoryResources, progress, tracer +func (_m *MockIncrementalSyncFn) Execute(ctx context.Context, repo repository.Versioned, previousRef string, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer) error { + ret := _m.Called(ctx, repo, previousRef, currentRef, repositoryResources, progress, tracer) if len(ret) == 0 { panic("no return value specified for Execute") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, repository.Versioned, string, string, resources.RepositoryResources, jobs.JobProgressRecorder) error); ok { - r0 = rf(ctx, repo, previousRef, currentRef, repositoryResources, progress) + if rf, ok := ret.Get(0).(func(context.Context, repository.Versioned, string, string, resources.RepositoryResources, jobs.JobProgressRecorder, tracing.Tracer) error); ok { + r0 = rf(ctx, repo, previousRef, currentRef, repositoryResources, progress, tracer) } else { r0 = ret.Error(0) } @@ -56,13 +58,14 @@ type MockIncrementalSyncFn_Execute_Call struct { // - currentRef string // - repositoryResources resources.RepositoryResources // - progress jobs.JobProgressRecorder -func (_e *MockIncrementalSyncFn_Expecter) Execute(ctx interface{}, repo interface{}, previousRef interface{}, currentRef interface{}, repositoryResources interface{}, progress interface{}) *MockIncrementalSyncFn_Execute_Call { - return &MockIncrementalSyncFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, previousRef, currentRef, repositoryResources, progress)} +// - tracer tracing.Tracer +func (_e *MockIncrementalSyncFn_Expecter) Execute(ctx interface{}, repo interface{}, previousRef interface{}, currentRef interface{}, repositoryResources interface{}, progress interface{}, tracer interface{}) *MockIncrementalSyncFn_Execute_Call { + return &MockIncrementalSyncFn_Execute_Call{Call: _e.mock.On("Execute", ctx, repo, previousRef, currentRef, repositoryResources, progress, tracer)} } -func (_c *MockIncrementalSyncFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Versioned, previousRef string, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder)) *MockIncrementalSyncFn_Execute_Call { +func (_c *MockIncrementalSyncFn_Execute_Call) Run(run func(ctx context.Context, repo repository.Versioned, previousRef string, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer)) *MockIncrementalSyncFn_Execute_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(repository.Versioned), args[2].(string), args[3].(string), args[4].(resources.RepositoryResources), args[5].(jobs.JobProgressRecorder)) + run(args[0].(context.Context), args[1].(repository.Versioned), args[2].(string), args[3].(string), args[4].(resources.RepositoryResources), args[5].(jobs.JobProgressRecorder), args[6].(tracing.Tracer)) }) return _c } @@ -72,7 +75,7 @@ func (_c *MockIncrementalSyncFn_Execute_Call) Return(_a0 error) *MockIncremental return _c } -func (_c *MockIncrementalSyncFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Versioned, string, string, resources.RepositoryResources, jobs.JobProgressRecorder) error) *MockIncrementalSyncFn_Execute_Call { +func (_c *MockIncrementalSyncFn_Execute_Call) RunAndReturn(run func(context.Context, repository.Versioned, string, string, resources.RepositoryResources, jobs.JobProgressRecorder, tracing.Tracer) error) *MockIncrementalSyncFn_Execute_Call { _c.Call.Return(run) return _c } diff --git a/pkg/registry/apis/provisioning/jobs/sync/incremental_test.go b/pkg/registry/apis/provisioning/jobs/sync/incremental_test.go index 290b40c77ce..6562be15f3c 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/incremental_test.go +++ b/pkg/registry/apis/provisioning/jobs/sync/incremental_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/grafana/grafana/apps/provisioning/pkg/repository" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/stretchr/testify/mock" @@ -29,7 +30,7 @@ func TestIncrementalSync_ContextCancelled(t *testing.T) { progress.On("SetTotal", mock.Anything, 1).Return() progress.On("SetMessage", mock.Anything, "replicating versioned changes").Return() - err := IncrementalSync(ctx, repo, "old-ref", "new-ref", repoResources, progress) + err := IncrementalSync(ctx, repo, "old-ref", "new-ref", repoResources, progress, tracing.NewNoopTracerService()) require.EqualError(t, err, "context canceled") } @@ -386,7 +387,7 @@ func TestIncrementalSync(t *testing.T) { tt.setupMocks(repo, repoResources, progress) - err := IncrementalSync(context.Background(), repo, tt.previousRef, tt.currentRef, repoResources, progress) + err := IncrementalSync(context.Background(), repo, tt.previousRef, tt.currentRef, repoResources, progress, tracing.NewNoopTracerService()) if tt.expectedError != "" { require.EqualError(t, err, tt.expectedError) diff --git a/pkg/registry/apis/provisioning/jobs/sync/sync.go b/pkg/registry/apis/provisioning/jobs/sync/sync.go index 541fd47a5f9..fa58d426cba 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/sync.go +++ b/pkg/registry/apis/provisioning/jobs/sync/sync.go @@ -6,18 +6,19 @@ import ( provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" ) //go:generate mockery --name FullSyncFn --structname MockFullSyncFn --inpackage --filename full_sync_fn_mock.go --with-expecter -type FullSyncFn func(ctx context.Context, repo repository.Reader, compare CompareFn, clients resources.ResourceClients, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error +type FullSyncFn func(ctx context.Context, repo repository.Reader, compare CompareFn, clients resources.ResourceClients, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer) error //go:generate mockery --name CompareFn --structname MockCompareFn --inpackage --filename compare_fn_mock.go --with-expecter type CompareFn func(ctx context.Context, repo repository.Reader, repositoryResources resources.RepositoryResources, ref string) ([]ResourceFileChange, error) //go:generate mockery --name IncrementalSyncFn --structname MockIncrementalSyncFn --inpackage --filename incremental_sync_fn_mock.go --with-expecter -type IncrementalSyncFn func(ctx context.Context, repo repository.Versioned, previousRef, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder) error +type IncrementalSyncFn func(ctx context.Context, repo repository.Versioned, previousRef, currentRef string, repositoryResources resources.RepositoryResources, progress jobs.JobProgressRecorder, tracer tracing.Tracer) error //go:generate mockery --name Syncer --structname MockSyncer --inpackage --filename syncer_mock.go --with-expecter type Syncer interface { @@ -28,13 +29,15 @@ type syncer struct { compare CompareFn fullSync FullSyncFn incrementalSync IncrementalSyncFn + tracer tracing.Tracer } -func NewSyncer(compare CompareFn, fullSync FullSyncFn, incrementalSync IncrementalSyncFn) Syncer { +func NewSyncer(compare CompareFn, fullSync FullSyncFn, incrementalSync IncrementalSyncFn, tracer tracing.Tracer) Syncer { return &syncer{ compare: compare, fullSync: fullSync, incrementalSync: incrementalSync, + tracer: tracer, } } @@ -52,11 +55,11 @@ func (r *syncer) Sync(ctx context.Context, repo repository.ReaderWriter, options if cfg.Status.Sync.LastRef != "" && options.Incremental { progress.SetMessage(ctx, "incremental sync") - return currentRef, r.incrementalSync(ctx, versionedRepo, cfg.Status.Sync.LastRef, currentRef, repositoryResources, progress) + return currentRef, r.incrementalSync(ctx, versionedRepo, cfg.Status.Sync.LastRef, currentRef, repositoryResources, progress, r.tracer) } } progress.SetMessage(ctx, "full sync") - return currentRef, r.fullSync(ctx, repo, r.compare, clients, currentRef, repositoryResources, progress) + return currentRef, r.fullSync(ctx, repo, r.compare, clients, currentRef, repositoryResources, progress, r.tracer) } diff --git a/pkg/registry/apis/provisioning/jobs/sync/sync_test.go b/pkg/registry/apis/provisioning/jobs/sync/sync_test.go index 1751159659a..5f04ed350e1 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/sync_test.go +++ b/pkg/registry/apis/provisioning/jobs/sync/sync_test.go @@ -7,6 +7,7 @@ import ( provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" mock "github.com/stretchr/testify/mock" @@ -65,7 +66,7 @@ func TestSyncer_Sync(t *testing.T) { repo.MockVersioned.On("LatestRef", mock.Anything).Return("new-ref", nil) progress.On("SetMessage", mock.Anything, "full sync").Return() - fullSyncFn.EXPECT().Execute(mock.Anything, mock.Anything, mock.Anything, mock.Anything, "new-ref", mock.Anything, mock.Anything).Return(nil) + fullSyncFn.EXPECT().Execute(mock.Anything, mock.Anything, mock.Anything, mock.Anything, "new-ref", mock.Anything, mock.Anything, mock.Anything).Return(nil) }, expectedMessages: []string{"full sync"}, }, @@ -87,7 +88,7 @@ func TestSyncer_Sync(t *testing.T) { }) repo.MockVersioned.On("LatestRef", mock.Anything).Return("new-ref", nil) progress.On("SetMessage", mock.Anything, "incremental sync").Return() - incrementalSyncFn.EXPECT().Execute(mock.Anything, mock.Anything, "old-ref", "new-ref", mock.Anything, mock.Anything).Return(nil) + incrementalSyncFn.EXPECT().Execute(mock.Anything, mock.Anything, "old-ref", "new-ref", mock.Anything, mock.Anything, mock.Anything).Return(nil) }, expectedRef: "new-ref", expectedMessages: []string{"incremental sync"}, @@ -130,7 +131,7 @@ func TestSyncer_Sync(t *testing.T) { }) repo.MockVersioned.On("LatestRef", mock.Anything).Return("new-ref", nil) progress.On("SetMessage", mock.Anything, "incremental sync").Return() - incrementalSyncFn.On("Execute", mock.Anything, mock.Anything, "old-ref", "new-ref", mock.Anything, mock.Anything).Return(fmt.Errorf("incremental sync failed")) + incrementalSyncFn.On("Execute", mock.Anything, mock.Anything, "old-ref", "new-ref", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("incremental sync failed")) }, expectedRef: "new-ref", expectedMessages: []string{"incremental sync"}, @@ -158,6 +159,7 @@ func TestSyncer_Sync(t *testing.T) { compareFn.Execute, fullSyncFn.Execute, incrementalSyncFn.Execute, + tracing.NewNoopTracerService(), ) ref, err := syncer.Sync(context.Background(), repo, tt.options, repoResources, clients, progress) diff --git a/pkg/registry/apis/provisioning/jobs/sync/worker.go b/pkg/registry/apis/provisioning/jobs/sync/worker.go index 87ef447ff7f..0b0139941c0 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/worker.go +++ b/pkg/registry/apis/provisioning/jobs/sync/worker.go @@ -3,13 +3,18 @@ package sync import ( "context" "fmt" + "time" "github.com/grafana/grafana-app-sdk/logging" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/utils" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) //go:generate mockery --name RepositoryPatchFn --structname MockRepositoryPatchFn --inpackage --filename repository_patch_fn_mock.go --with-expecter @@ -32,6 +37,10 @@ type SyncWorker struct { // Sync functions syncer Syncer + + metrics jobs.JobMetrics + + tracer tracing.Tracer } func NewSyncWorker( @@ -40,6 +49,8 @@ func NewSyncWorker( storageStatus dualwrite.Service, patchStatus RepositoryPatchFn, syncer Syncer, + metrics jobs.JobMetrics, + tracer tracing.Tracer, ) *SyncWorker { return &SyncWorker{ clients: clients, @@ -47,6 +58,8 @@ func NewSyncWorker( patchStatus: patchStatus, storageStatus: storageStatus, syncer: syncer, + metrics: metrics, + tracer: tracer, } } @@ -57,15 +70,39 @@ func (r *SyncWorker) IsSupported(ctx context.Context, job provisioning.Job) bool func (r *SyncWorker) Process(ctx context.Context, repo repository.Repository, job provisioning.Job, progress jobs.JobProgressRecorder) error { cfg := repo.Config() logger := logging.FromContext(ctx).With("job", job.GetName(), "namespace", job.GetNamespace()) + ctx, span := r.tracer.Start(ctx, "provisioning.sync.process", + trace.WithAttributes( + attribute.String("job.name", job.GetName()), + attribute.String("job.namespace", job.GetNamespace()), + attribute.String("job.action", string(job.Spec.Action)), + attribute.String("repository.name", cfg.Name), + attribute.String("repository.namespace", cfg.Namespace), + ), + ) + defer span.End() + + start := time.Now() + outcome := utils.ErrorOutcome + totalChangesMade := 0 + defer func() { + r.metrics.RecordJob(string(provisioning.JobActionPull), outcome, totalChangesMade, time.Since(start).Seconds()) + span.SetAttributes( + attribute.String("outcome", outcome), + attribute.Int("changes_made", totalChangesMade), + ) + }() + // Check if we are onboarding from legacy storage // HACK -- this should be handled outside of this worker if r.storageStatus != nil && dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, r.storageStatus) { - return fmt.Errorf("sync not supported until storage has migrated") + err := fmt.Errorf("sync not supported until storage has migrated") + return tracing.Error(span, err) } rw, ok := repo.(repository.ReaderWriter) if !ok { - return fmt.Errorf("sync job submitted for repository that does not support read-write -- this is a bug") + err := fmt.Errorf("sync job submitted for repository that does not support read-write") + return tracing.Error(span, err) } syncStatus := job.Status.ToSyncStatus(job.Name) @@ -83,26 +120,49 @@ func (r *SyncWorker) Process(ctx context.Context, repo repository.Repository, jo } progress.SetMessage(ctx, "update sync status at start") - if err := r.patchStatus(ctx, cfg, patchOperations...); err != nil { - return fmt.Errorf("update repo with job status at start: %w", err) - } - repositoryResources, err := r.repositoryResources.Client(ctx, rw) + statusCtx, statusSpan := r.tracer.Start(ctx, "provisioning.sync.update_start_status") + if err := r.patchStatus(statusCtx, cfg, patchOperations...); err != nil { + statusSpan.End() + logger.Error("failed to update the repository status at the start of the sync job", "error", err) + err = fmt.Errorf("update repo with job status at start: %w", err) + return tracing.Error(span, err) + } + statusSpan.End() + + setupCtx, setupSpan := r.tracer.Start(ctx, "provisioning.sync.setup_clients") + repositoryResources, err := r.repositoryResources.Client(setupCtx, rw) if err != nil { - return fmt.Errorf("create repository resources client: %w", err) + setupSpan.End() + logger.Error("failed to create repository resources client", "error", err) + err = fmt.Errorf("create repository resources client: %w", err) + return tracing.Error(span, err) } - - clients, err := r.clients.Clients(ctx, cfg.Namespace) + clients, err := r.clients.Clients(setupCtx, cfg.Namespace) if err != nil { - return fmt.Errorf("get clients for %s: %w", cfg.Name, err) + setupSpan.End() + logger.Error("failed to get clients for the repository", "error", err) + err = fmt.Errorf("get clients for %s: %w", cfg.Name, err) + return tracing.Error(span, err) } + setupSpan.End() + syncCtx, syncSpan := r.tracer.Start(ctx, "provisioning.sync.execute") progress.SetMessage(ctx, "execute sync job") progress.StrictMaxErrors(20) // make it stop after 20 errors - - currentRef, syncError := r.syncer.Sync(ctx, rw, *job.Spec.Pull, repositoryResources, clients, progress) + currentRef, syncError := r.syncer.Sync(syncCtx, rw, *job.Spec.Pull, repositoryResources, clients, progress) jobStatus := progress.Complete(ctx, syncError) syncStatus = jobStatus.ToSyncStatus(job.Name) + if syncError != nil { + logger.Debug("failed to sync the repository", "error", syncError) + _ = tracing.Error(syncSpan, syncError) + } else { + outcome = utils.SuccessOutcome + for _, summary := range jobStatus.Summary { + totalChangesMade += int(summary.Create + summary.Update + summary.Delete) + } + } + syncSpan.End() // Create sync status and set hash if successful if syncStatus.State == provisioning.JobStateSuccess { @@ -121,13 +181,17 @@ func (r *SyncWorker) Process(ctx context.Context, repo repository.Repository, jo }, } + finalCtx, finalSpan := r.tracer.Start(ctx, "provisioning.sync.update_final_status") + // Only add stats patch if stats are not nil - stats, err := repositoryResources.Stats(ctx) + stats, err := repositoryResources.Stats(finalCtx) switch { case err != nil: logger.Error("unable to read stats", "error", err) + finalSpan.SetAttributes(attribute.String("stats.error", err.Error())) case stats == nil: logger.Error("stats are nil") + finalSpan.SetAttributes(attribute.Bool("stats.nil", true)) case len(stats.Managed) == 1: patchOperations = append(patchOperations, map[string]interface{}{ "op": "replace", @@ -136,12 +200,17 @@ func (r *SyncWorker) Process(ctx context.Context, repo repository.Repository, jo }) default: logger.Warn("unexpected number of managed stats", "count", len(stats.Managed)) + finalSpan.SetAttributes(attribute.Int("stats.unexpected_count", len(stats.Managed))) } // Only patch the specific fields we want to update, not the entire status - if err := r.patchStatus(ctx, cfg, patchOperations...); err != nil { - return fmt.Errorf("update repo with job final status: %w", err) + if err := r.patchStatus(finalCtx, cfg, patchOperations...); err != nil { + finalSpan.End() + logger.Error("failed to update the repository status at the end of the sync job", "error", err) + err = fmt.Errorf("update repo with job final status: %w", err) + return tracing.Error(span, err) } + finalSpan.End() return syncError } diff --git a/pkg/registry/apis/provisioning/jobs/sync/worker_test.go b/pkg/registry/apis/provisioning/jobs/sync/worker_test.go index 901c577aab8..090d4deac87 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/sync/worker_test.go @@ -7,15 +7,18 @@ import ( provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestSyncWorker_IsSupported(t *testing.T) { + metrics := jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()) tests := []struct { name string job provisioning.Job @@ -43,7 +46,7 @@ func TestSyncWorker_IsSupported(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - worker := NewSyncWorker(nil, nil, nil, nil, nil) + worker := NewSyncWorker(nil, nil, nil, nil, nil, metrics, tracing.NewNoopTracerService()) result := worker.IsSupported(context.Background(), tt.job) require.Equal(t, tt.expected, result) }) @@ -62,9 +65,9 @@ func TestSyncWorker_ProcessNotReaderWriter(t *testing.T) { }) fakeDualwrite := dualwrite.NewMockService(t) fakeDualwrite.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil).Twice() - worker := NewSyncWorker(nil, nil, fakeDualwrite, nil, nil) + worker := NewSyncWorker(nil, nil, fakeDualwrite, nil, nil, jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()), tracing.NewNoopTracerService()) err := worker.Process(context.Background(), repo, provisioning.Job{}, jobs.NewMockJobProgressRecorder(t)) - require.EqualError(t, err, "sync job submitted for repository that does not support read-write -- this is a bug") + require.EqualError(t, err, "sync job submitted for repository that does not support read-write") } func TestSyncWorker_Process(t *testing.T) { @@ -530,6 +533,8 @@ func TestSyncWorker_Process(t *testing.T) { dualwriteService, repositoryPatchFn.Execute, syncer, + jobs.RegisterJobMetrics(prometheus.NewPedanticRegistry()), + tracing.NewNoopTracerService(), ) // Create test job diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index cd353ba5ccd..920c8680b21 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -22,6 +22,7 @@ import ( "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" + clientrest "k8s.io/client-go/rest" "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" @@ -114,6 +115,9 @@ type APIBuilder struct { // Extras provides additional functionality to the API. extras []Extra extraWorkers []jobs.Worker + + restConfigGetter func(context.Context) (*clientrest.Config, error) + registry prometheus.Registerer } // NewAPIBuilder creates an API builder. @@ -134,7 +138,9 @@ func NewAPIBuilder( extraWorkers []jobs.Worker, jobHistoryConfig *JobHistoryConfig, allowedTargets []provisioning.SyncTargetType, + restConfigGetter func(context.Context) (*clientrest.Config, error), allowImageRendering bool, + registry prometheus.Registerer, newStandaloneClientFactoryFunc func(loopbackConfigProvider apiserver.RestConfigProvider) resources.ClientFactory, // optional, only used for standalone apiserver ) *APIBuilder { var clients resources.ClientFactory @@ -163,7 +169,9 @@ func NewAPIBuilder( jobHistoryConfig: jobHistoryConfig, extraWorkers: extraWorkers, allowedTargets: allowedTargets, + restConfigGetter: restConfigGetter, allowImageRendering: allowImageRendering, + registry: registry, } for _, builder := range extraBuilders { @@ -247,7 +255,9 @@ func RegisterAPIService( extraWorkers, createJobHistoryConfigFromSettings(cfg), allowedTargets, + nil, // will use loopback instead cfg.ProvisioningAllowImageRendering, + reg, nil, ) apiregistration.RegisterAPI(builder) @@ -678,7 +688,17 @@ func (b *APIBuilder) verifyAgainstExistingRepositories(cfg *provisioning.Reposit func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartHookFunc, error) { postStartHooks := map[string]genericapiserver.PostStartHookFunc{ "grafana-provisioning": func(postStartHookCtx genericapiserver.PostStartHookContext) error { - c, err := clientset.NewForConfig(postStartHookCtx.LoopbackClientConfig) + var config *clientrest.Config + var err error + if b.restConfigGetter == nil { + config = postStartHookCtx.LoopbackClientConfig + } else { + config, err = b.restConfigGetter(postStartHookCtx.Context) + if err != nil { + return err + } + } + c, err := clientset.NewForConfig(config) if err != nil { return err } @@ -691,13 +711,13 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH b.client = c.ProvisioningV0alpha1() // Initialize the API client-based job store - b.jobs, err = jobs.NewJobStore(b.client, 30*time.Second) + b.jobs, err = jobs.NewJobStore(b.client, 30*time.Second, b.registry) if err != nil { return fmt.Errorf("create API client job store: %w", err) } b.statusPatcher = appcontroller.NewRepositoryStatusPatcher(b.GetClient()) - b.healthChecker = controller.NewHealthChecker(b.statusPatcher) + b.healthChecker = controller.NewHealthChecker(b.statusPatcher, b.registry) // if running solely CRUD, skip the rest of the setup if b.onlyApiServer { @@ -717,21 +737,26 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH usageMetricCollector := usage.MetricCollector(b.tracer, b.getRepositoriesInNamespace, b.unified) b.usageStats.RegisterMetricsFunc(usageMetricCollector) + metrics := jobs.RegisterJobMetrics(b.registry) + stageIfPossible := repository.WrapWithStageAndPushIfPossible exportWorker := export.NewExportWorker( b.clients, b.repositoryResources, export.ExportAll, stageIfPossible, + metrics, ) - syncer := sync.NewSyncer(sync.Compare, sync.FullSync, sync.IncrementalSync) + syncer := sync.NewSyncer(sync.Compare, sync.FullSync, sync.IncrementalSync, b.tracer) syncWorker := sync.NewSyncWorker( b.clients, b.repositoryResources, b.storageStatus, b.statusPatcher.Patch, syncer, + metrics, + b.tracer, ) signerFactory := signature.NewSignerFactory(b.clients) legacyResources := migrate.NewLegacyResourcesMigrator( @@ -763,8 +788,8 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH b.storageStatus, ) - deleteWorker := deletepkg.NewWorker(syncWorker, stageIfPossible, b.repositoryResources) - moveWorker := movepkg.NewWorker(syncWorker, stageIfPossible, b.repositoryResources) + deleteWorker := deletepkg.NewWorker(syncWorker, stageIfPossible, b.repositoryResources, metrics) + moveWorker := movepkg.NewWorker(syncWorker, stageIfPossible, b.repositoryResources, metrics) workers := []jobs.Worker{ deleteWorker, exportWorker, @@ -799,6 +824,7 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH 30*time.Second, // Lease renewal interval b.jobs, repoGetter, jobHistoryWriter, jobController.InsertNotifications(), + b.registry, workers..., ) if err != nil { @@ -821,6 +847,8 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH b.storageStatus, b.GetHealthChecker(), b.statusPatcher, + b.registry, + b.tracer, ) if err != nil { return err diff --git a/pkg/registry/apis/provisioning/utils/metrics.go b/pkg/registry/apis/provisioning/utils/metrics.go new file mode 100644 index 00000000000..95171aed588 --- /dev/null +++ b/pkg/registry/apis/provisioning/utils/metrics.go @@ -0,0 +1,25 @@ +package utils + +const ( + SuccessOutcome = "success" + ErrorOutcome = "error" +) + +func GetResourceCountBucket(count int) string { + switch { + case count == 0: + return "0" + case count <= 10: + return "1-10" + case count <= 50: + return "11-50" + case count <= 100: + return "51-100" + case count <= 500: + return "101-500" + case count <= 1000: + return "501-1000" + default: + return "1000+" + } +} diff --git a/pkg/registry/apis/provisioning/webhooks/metrics.go b/pkg/registry/apis/provisioning/webhooks/metrics.go new file mode 100644 index 00000000000..1323c12233d --- /dev/null +++ b/pkg/registry/apis/provisioning/webhooks/metrics.go @@ -0,0 +1,30 @@ +package webhooks + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +type webhookMetrics struct { + registry prometheus.Registerer + eventsProcessed *prometheus.CounterVec +} + +func registerWebhookMetrics(registry prometheus.Registerer) webhookMetrics { + eventsProcessed := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "grafana_provisioning_webhook_events_processed_total", + Help: "Total number of webhook events processed and what job type was queued", + }, + []string{"job"}, + ) + registry.MustRegister(eventsProcessed) + + return webhookMetrics{ + registry: registry, + eventsProcessed: eventsProcessed, + } +} + +func (m *webhookMetrics) recordEventProcessed(jobQueued string) { + m.eventsProcessed.WithLabelValues(jobQueued).Inc() +} diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/changes.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/changes.go index 6f50fbfba31..4c9a49a7db4 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/changes.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/changes.go @@ -6,6 +6,7 @@ import ( "net/url" "path" "strings" + "time" "github.com/grafana/grafana-app-sdk/logging" dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" @@ -15,6 +16,8 @@ import ( "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/utils" + "github.com/prometheus/client_golang/prometheus" ) type changeInfo struct { @@ -53,13 +56,16 @@ type evaluator struct { render ScreenshotRenderer parsers resources.ParserFactory urlProvider func(namespace string) string + metrics screenshotMetrics } -func NewEvaluator(render ScreenshotRenderer, parsers resources.ParserFactory, urlProvider func(namespace string) string) Evaluator { +func NewEvaluator(render ScreenshotRenderer, parsers resources.ParserFactory, urlProvider func(namespace string) string, registry prometheus.Registerer) Evaluator { + metrics := registerScreenshotMetrics(registry) return &evaluator{ render: render, parsers: parsers, urlProvider: urlProvider, + metrics: metrics, } } @@ -152,14 +158,14 @@ func (e *evaluator) evaluateFile(ctx context.Context, repo repository.Reader, ba info.PreviewURL += "?" + query.Encode() if shouldRender { if info.GrafanaURL != "" { - info.GrafanaScreenshotURL, err = renderScreenshotFromGrafanaURL(ctx, baseURL, e.render, info.Parsed.Repo, info.GrafanaURL) + info.GrafanaScreenshotURL, err = renderScreenshotFromGrafanaURL(ctx, baseURL, e.render, info.Parsed.Repo, info.GrafanaURL, e.metrics) if err != nil { info.Error = err.Error() } } if info.PreviewURL != "" { - info.PreviewScreenshotURL, err = renderScreenshotFromGrafanaURL(ctx, baseURL, e.render, info.Parsed.Repo, info.PreviewURL) + info.PreviewScreenshotURL, err = renderScreenshotFromGrafanaURL(ctx, baseURL, e.render, info.Parsed.Repo, info.PreviewURL, e.metrics) if err != nil { info.Error = err.Error() } @@ -175,7 +181,13 @@ func renderScreenshotFromGrafanaURL(ctx context.Context, renderer ScreenshotRenderer, repo provisioning.ResourceRepositoryInfo, grafanaURL string, + metrics screenshotMetrics, ) (string, error) { + outcome := utils.ErrorOutcome + duration := time.Now() + defer func() { + metrics.recordScreenshotDuration(outcome, time.Since(duration)) + }() parsed, err := url.Parse(grafanaURL) if err != nil { logging.FromContext(ctx).Warn("invalid", "url", grafanaURL, "err", err) @@ -194,5 +206,6 @@ func renderScreenshotFromGrafanaURL(ctx context.Context, logger.Warn("invalid base", "url", baseURL, "err", err) return "", err } + outcome = utils.SuccessOutcome return base.JoinPath(snap).String(), nil } diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/changes_test.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/changes_test.go index fef0cebfc16..1d647256a2c 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/changes_test.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/changes_test.go @@ -7,6 +7,7 @@ import ( "fmt" "testing" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -759,7 +760,7 @@ func TestCalculateChanges(t *testing.T) { } return "http://host/" - }) + }, prometheus.NewPedanticRegistry()) pullRequest := provisioning.PullRequestJobOptions{ Ref: "ref", @@ -890,6 +891,7 @@ func TestRenderScreenshotFromGrafanaURL(t *testing.T) { }, } + metrics := registerScreenshotMetrics(prometheus.NewPedanticRegistry()) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { renderer := NewMockScreenshotRenderer(t) @@ -900,7 +902,7 @@ func TestRenderScreenshotFromGrafanaURL(t *testing.T) { Name: "repo", } - got, err := renderScreenshotFromGrafanaURL(context.Background(), tt.baseURL, renderer, repo, tt.grafanaURL) + got, err := renderScreenshotFromGrafanaURL(context.Background(), tt.baseURL, renderer, repo, tt.grafanaURL, metrics) if tt.wantErr != "" { require.Error(t, err) require.Contains(t, err.Error(), tt.wantErr) diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/metrics.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/metrics.go new file mode 100644 index 00000000000..651f07795bb --- /dev/null +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/metrics.go @@ -0,0 +1,97 @@ +package pullrequest + +import ( + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + pullRequestMetricsOnce sync.Once + pullRequestMetricsInstance pullRequestMetrics +) + +type pullRequestMetrics struct { + registry prometheus.Registerer + processingDuration *prometheus.HistogramVec + commentsPosted *prometheus.CounterVec +} + +func registerPullRequestMetrics(registry prometheus.Registerer) pullRequestMetrics { + // called by both ProvidePullRequestWorker and ProvideWebhooksWithImages + // this ensures we only register the metric once + pullRequestMetricsOnce.Do(func() { + processingDuration := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "grafana_provisioning_pullrequest_processing_duration_seconds", + Help: "Duration of pull request processing", + Buckets: []float64{0.5, 1.0, 2.0, 5.0, 10.0, 30.0}, + }, + []string{"outcome"}, + ) + registry.MustRegister(processingDuration) + + commentsPosted := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "grafana_provisioning_pullrequest_comments_posted_total", + Help: "Total number of comments posted to pull requests", + }, + []string{"outcome"}, + ) + registry.MustRegister(commentsPosted) + + pullRequestMetricsInstance = pullRequestMetrics{ + registry: registry, + processingDuration: processingDuration, + commentsPosted: commentsPosted, + } + }) + + return pullRequestMetricsInstance +} + +func (m *pullRequestMetrics) recordProcessed(outcome string, duration time.Duration) { + m.processingDuration.WithLabelValues(outcome).Observe(duration.Seconds()) +} + +func (m *pullRequestMetrics) recordCommentPosted(outcome string) { + m.commentsPosted.WithLabelValues(outcome).Inc() +} + +type screenshotMetrics struct { + registry prometheus.Registerer + screenshotDuration *prometheus.HistogramVec +} + +var ( + screenshotMetricsOnce sync.Once + screenshotMetricsInstance screenshotMetrics +) + +func registerScreenshotMetrics(registry prometheus.Registerer) screenshotMetrics { + // called by both ProvidePullRequestWorker and ProvideWebhooksWithImages + // this ensures we only register the metric once + screenshotMetricsOnce.Do(func() { + screenshotDuration := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "grafana_provisioning_pullrequest_screenshot_duration_seconds", + Help: "Duration of screenshot generation", + Buckets: []float64{1.0, 2.0, 5.0, 10.0, 30.0, 60.0}, + }, + []string{"outcome"}, + ) + registry.MustRegister(screenshotDuration) + + screenshotMetricsInstance = screenshotMetrics{ + registry: registry, + screenshotDuration: screenshotDuration, + } + }) + + return screenshotMetricsInstance +} + +func (m *screenshotMetrics) recordScreenshotDuration(outcome string, duration time.Duration) { + m.screenshotDuration.WithLabelValues(outcome).Observe(duration.Seconds()) +} diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/worker.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/worker.go index 1291c590148..299febe9786 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/worker.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/worker.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -12,10 +13,12 @@ import ( "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/utils" "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/prometheus/client_golang/prometheus" ) func ProvidePullRequestWorker( @@ -23,6 +26,7 @@ func ProvidePullRequestWorker( renderer rendering.Service, blobstore resource.ResourceClient, configProvider apiserver.RestConfigProvider, + registry prometheus.Registerer, ) *PullRequestWorker { urlProvider := func(_ string) string { return cfg.AppURL @@ -33,10 +37,10 @@ func ProvidePullRequestWorker( clients := resources.NewClientFactory(configProvider) parsers := resources.NewParserFactory(clients) screenshotRenderer := NewScreenshotRenderer(renderer, blobstore) - evaluator := NewEvaluator(screenshotRenderer, parsers, urlProvider) + evaluator := NewEvaluator(screenshotRenderer, parsers, urlProvider, registry) commenter := NewCommenter() - return NewPullRequestWorker(evaluator, commenter) + return NewPullRequestWorker(evaluator, commenter, registry) } //go:generate mockery --name=PullRequestRepo --structname=MockPullRequestRepo --inpackage --filename=mock_pullrequest_repo.go --with-expecter @@ -60,12 +64,15 @@ type Commenter interface { type PullRequestWorker struct { evaluator Evaluator commenter Commenter + metrics pullRequestMetrics } -func NewPullRequestWorker(evaluator Evaluator, commenter Commenter) *PullRequestWorker { +func NewPullRequestWorker(evaluator Evaluator, commenter Commenter, registry prometheus.Registerer) *PullRequestWorker { + metrics := registerPullRequestMetrics(registry) return &PullRequestWorker{ evaluator: evaluator, commenter: commenter, + metrics: metrics, } } @@ -80,30 +87,42 @@ func (c *PullRequestWorker) Process(ctx context.Context, ) error { cfg := repo.Config().Spec opts := job.Spec.PullRequest + startTime := time.Now() + outcome := utils.ErrorOutcome + defer func() { + duration := time.Since(startTime) + c.metrics.recordProcessed(outcome, duration) + }() + if opts == nil { return apierrors.NewBadRequest("missing spec.pr") } + logger := logging.FromContext(ctx).With("pr", opts.PR, "repo", repo.Config().GetName(), "namespace", job.GetNamespace()) + if opts.Ref == "" { + logger.Debug("missing spec.ref") return apierrors.NewBadRequest("missing spec.ref") } // FIXME: this is leaky because it's supposed to be already a PullRequestRepo if cfg.GitHub == nil { + logger.Debug("expecting github configuration") return apierrors.NewBadRequest("expecting github configuration") } reader, ok := repo.(repository.Reader) if !ok { + logger.Debug("pull request job submitted targeting repository that is not a Reader") return errors.New("pull request job submitted targeting repository that is not a Reader") } prRepo, ok := repo.(PullRequestRepo) if !ok { + logger.Debug("pull request job submitted targeting repository that is not a PullRequestRepo") return fmt.Errorf("repository is not a pull request repository") } - logger := logging.FromContext(ctx).With("pr", opts.PR) logger.Info("process pull request") defer logger.Info("pull request processed") @@ -112,6 +131,7 @@ func (c *PullRequestWorker) Process(ctx context.Context, base := cfg.GitHub.Branch files, err := prRepo.CompareFiles(ctx, base, opts.Ref) if err != nil { + logger.Error("failed to list pull request files", "error", err) return fmt.Errorf("failed to list pull request files: %w", err) } @@ -123,12 +143,16 @@ func (c *PullRequestWorker) Process(ctx context.Context, changeInfo, err := c.evaluator.Evaluate(ctx, reader, *opts, files, progress) if err != nil { + logger.Error("failed to calculate changes", "error", err) return fmt.Errorf("calculate changes: %w", err) } if err := c.commenter.Comment(ctx, prRepo, opts.PR, changeInfo); err != nil { + c.metrics.recordCommentPosted(utils.ErrorOutcome) return fmt.Errorf("comment pull request: %w", err) } + outcome = utils.SuccessOutcome + c.metrics.recordCommentPosted(utils.SuccessOutcome) logger.Info("preview comment added") return nil diff --git a/pkg/registry/apis/provisioning/webhooks/pullrequest/worker_test.go b/pkg/registry/apis/provisioning/webhooks/pullrequest/worker_test.go index 7f1cb4f14a0..3414703a5bf 100644 --- a/pkg/registry/apis/provisioning/webhooks/pullrequest/worker_test.go +++ b/pkg/registry/apis/provisioning/webhooks/pullrequest/worker_test.go @@ -5,10 +5,12 @@ import ( "errors" "testing" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/grafana/grafana-app-sdk/logging" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" @@ -44,7 +46,7 @@ func TestPullRequestWorker_IsSupported(t *testing.T) { t.Run(tt.name, func(t *testing.T) { evaluator := NewMockEvaluator(t) commenter := NewMockCommenter(t) - worker := NewPullRequestWorker(evaluator, commenter) + worker := NewPullRequestWorker(evaluator, commenter, prometheus.NewPedanticRegistry()) result := worker.IsSupported(context.Background(), tt.job) require.Equal(t, tt.expected, result) }) @@ -68,7 +70,7 @@ func TestPullRequestWorker_Process_NotPullRequestRepository(t *testing.T) { }, }) - worker := NewPullRequestWorker(evaluator, commenter) + worker := NewPullRequestWorker(evaluator, commenter, prometheus.NewPedanticRegistry()) job := provisioning.Job{ Spec: provisioning.JobSpec{ Action: provisioning.JobActionPullRequest, @@ -106,7 +108,7 @@ func TestPullRequestWorker_Process_NotReaderRepository(t *testing.T) { }, }) - worker := NewPullRequestWorker(evaluator, commenter) + worker := NewPullRequestWorker(evaluator, commenter, prometheus.NewPedanticRegistry()) job := provisioning.Job{ Spec: provisioning.JobSpec{ Action: provisioning.JobActionPullRequest, @@ -392,7 +394,7 @@ func TestPullRequestWorker_Process(t *testing.T) { progress := jobs.NewMockJobProgressRecorder(t) tt.setupMocks(evaluator, commenter, &repo, progress) - worker := NewPullRequestWorker(evaluator, commenter) + worker := NewPullRequestWorker(evaluator, commenter, prometheus.NewPedanticRegistry()) job := provisioning.Job{ Spec: provisioning.JobSpec{ Action: provisioning.JobActionPullRequest, @@ -400,7 +402,7 @@ func TestPullRequestWorker_Process(t *testing.T) { }, } - err := worker.Process(context.Background(), repo, job, progress) + err := worker.Process(logging.Context(context.Background(), logging.DefaultLogger), repo, job, progress) if tt.expectedError != "" { require.EqualError(t, err, tt.expectedError) } else { diff --git a/pkg/registry/apis/provisioning/webhooks/register.go b/pkg/registry/apis/provisioning/webhooks/register.go index ce357188bad..4fc02db9041 100644 --- a/pkg/registry/apis/provisioning/webhooks/register.go +++ b/pkg/registry/apis/provisioning/webhooks/register.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/prometheus/client_golang/prometheus" ) // WebhookExtraBuilder is a function that returns an ExtraBuilder. @@ -63,6 +64,7 @@ func ProvideWebhooksWithImages( renderer rendering.Service, blobstore resource.ResourceClient, configProvider apiserver.RestConfigProvider, + registry prometheus.Registerer, ) *WebhookExtraBuilder { urlProvider := func(_ string) string { return cfg.AppURL @@ -82,11 +84,12 @@ func ProvideWebhooksWithImages( isPublic, b, screenshotRenderer, + registry, ) - evaluator := pullrequest.NewEvaluator(screenshotRenderer, parsers, urlProvider) + evaluator := pullrequest.NewEvaluator(screenshotRenderer, parsers, urlProvider, registry) commenter := pullrequest.NewCommenter() - pullRequestWorker := pullrequest.NewPullRequestWorker(evaluator, commenter) + pullRequestWorker := pullrequest.NewPullRequestWorker(evaluator, commenter, registry) return NewWebhookExtraWithImages( render, @@ -98,7 +101,7 @@ func ProvideWebhooksWithImages( } } -func ProvideWebhooks(provisioningURL string) *WebhookExtraBuilder { +func ProvideWebhooks(provisioningURL string, registry prometheus.Registerer) *WebhookExtraBuilder { urlProvider := func(_ string) string { return provisioningURL } @@ -110,7 +113,7 @@ func ProvideWebhooks(provisioningURL string) *WebhookExtraBuilder { urlProvider: urlProvider, ExtraBuilder: func(b *provisioningapis.APIBuilder) provisioningapis.Extra { screenshotRenderer := pullrequest.NewNoOpRenderer() - webhook := NewWebhookConnector(isPublic, b, screenshotRenderer) + webhook := NewWebhookConnector(isPublic, b, screenshotRenderer, registry) return NewWebhookExtra(webhook) }, diff --git a/pkg/registry/apis/provisioning/webhooks/webhook.go b/pkg/registry/apis/provisioning/webhooks/webhook.go index 0997818d692..4e5bfffb28a 100644 --- a/pkg/registry/apis/provisioning/webhooks/webhook.go +++ b/pkg/registry/apis/provisioning/webhooks/webhook.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" provisioningapis "github.com/grafana/grafana/pkg/registry/apis/provisioning" "github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks/pullrequest" + "github.com/prometheus/client_golang/prometheus" ) type WebhookRepository interface { @@ -34,6 +35,8 @@ type webhookConnector struct { webhooksEnabled bool core *provisioningapis.APIBuilder renderer pullrequest.ScreenshotRenderer + registry prometheus.Registerer + metrics webhookMetrics } func NewWebhookConnector( @@ -41,11 +44,15 @@ func NewWebhookConnector( // TODO: use interface for this core *provisioningapis.APIBuilder, renderer pullrequest.ScreenshotRenderer, + registry prometheus.Registerer, ) *webhookConnector { + metrics := registerWebhookMetrics(registry) return &webhookConnector{ webhooksEnabled: webhooksEnabled, core: core, renderer: renderer, + registry: registry, + metrics: metrics, } } @@ -147,10 +154,17 @@ func (s *webhookConnector) Connect(ctx context.Context, name string, opts runtim logger.Error("failed to update last event", "error", err) } + actionTaken := "none" + defer func() { + s.metrics.recordEventProcessed(actionTaken) + }() + if rsp.Job != nil { rsp.Job.Repository = name + actionTaken = string(rsp.Job.Action) job, err := s.core.GetJobQueue().Insert(ctx, namespace, *rsp.Job) if err != nil { + logger.Error("failed to insert job", "error", err) responder.Error(err) return } diff --git a/pkg/registry/apis/query/client/instance_provider.go b/pkg/registry/apis/query/client/instance_provider.go index 8b9a59e9672..e48e2e9a084 100644 --- a/pkg/registry/apis/query/client/instance_provider.go +++ b/pkg/registry/apis/query/client/instance_provider.go @@ -21,6 +21,7 @@ type singleTenantInstanceProvider struct { type singleTenantInstance struct { client clientapi.QueryDataClient instanceConf clientapi.InstanceConfigurationSettings + logger log.Logger } func (t *singleTenantInstance) GetDataSourceClient(_ context.Context, _ data.DataSourceRef) (clientapi.QueryDataClient, error) { @@ -43,10 +44,11 @@ func NewSingleTenantInstanceProvider(cfg *setting.Cfg, features featuremgmt.Feat } } -func (s *singleTenantInstanceProvider) GetInstance(_ context.Context, _ map[string]string) (clientapi.Instance, error) { +func (s *singleTenantInstanceProvider) GetInstance(_ context.Context, logger log.Logger, _ map[string]string) (clientapi.Instance, error) { return &singleTenantInstance{ client: s.client, instanceConf: s.instanceConf, + logger: logger, }, nil } @@ -54,9 +56,8 @@ func (s *singleTenantInstance) GetSettings() clientapi.InstanceConfigurationSett return s.instanceConf } -func (s *singleTenantInstance) GetLogger(parent log.Logger) log.Logger { - // currently we do not add any extra info - return parent.New() +func (s *singleTenantInstance) GetLogger() log.Logger { + return s.logger } func (s *singleTenantInstance) ReportMetrics() { diff --git a/pkg/registry/apis/query/clientapi/clientapi.go b/pkg/registry/apis/query/clientapi/clientapi.go index 3a3ec7fed7f..5d44045f77f 100644 --- a/pkg/registry/apis/query/clientapi/clientapi.go +++ b/pkg/registry/apis/query/clientapi/clientapi.go @@ -33,10 +33,10 @@ type Instance interface { GetDataSourceClient(ctx context.Context, ref data.DataSourceRef) (QueryDataClient, error) // fetch information on the grafana instance (e.g. feature toggles) GetSettings() InstanceConfigurationSettings - GetLogger(parent log.Logger) log.Logger - ReportMetrics() // some metrics are only reported at the end + GetLogger() log.Logger // returns the instance's logger. this logs instance-specific data too + ReportMetrics() // some metrics are only reported at the end } type InstanceProvider interface { - GetInstance(ctx context.Context, headers map[string]string) (Instance, error) + GetInstance(ctx context.Context, logger log.Logger, headers map[string]string) (Instance, error) } diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index 4ad089e014f..f63cb850bc7 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -277,7 +277,7 @@ func handleQuery(ctx context.Context, raw query.QueryDataRequest, b QueryAPIBuil headers := ExtractKnownHeaders(httpreq.Header) - instance, err := b.instanceProvider.GetInstance(ctx, headers) + instance, err := b.instanceProvider.GetInstance(ctx, connectLogger, headers) if err != nil { connectLogger.Error("failed to get instance configuration settings", "err", err) responder.Error(err) @@ -286,7 +286,7 @@ func handleQuery(ctx context.Context, raw query.QueryDataRequest, b QueryAPIBuil instanceConfig := instance.GetSettings() - dsQuerierLoggerWithSlug := instance.GetLogger(connectLogger) + dsQuerierLoggerWithSlug := instance.GetLogger() qsDsClientBuilder := dsquerierclient.NewQsDatasourceClientBuilderWithInstance( instance, diff --git a/pkg/registry/apis/query/query_test.go b/pkg/registry/apis/query/query_test.go index 431e03f22a2..f94cc92f317 100644 --- a/pkg/registry/apis/query/query_test.go +++ b/pkg/registry/apis/query/query_test.go @@ -259,11 +259,13 @@ func (m *mockResponder) Error(err error) { type mockClient struct { stubbedFrame *data.Frame + logger log.Logger } -func (m mockClient) GetInstance(ctx context.Context, headers map[string]string) (clientapi.Instance, error) { +func (m mockClient) GetInstance(ctx context.Context, logger log.Logger, headers map[string]string) (clientapi.Instance, error) { mclient := mockClient{ stubbedFrame: m.stubbedFrame, + logger: logger, } return mclient, nil } @@ -271,8 +273,8 @@ func (m mockClient) GetInstance(ctx context.Context, headers map[string]string) func (m mockClient) ReportMetrics() { } -func (m mockClient) GetLogger(parent log.Logger) log.Logger { - return parent.New() +func (m mockClient) GetLogger() log.Logger { + return m.logger } func (m mockClient) GetDataSourceClient(ctx context.Context, ref dataapi.DataSourceRef) (clientapi.QueryDataClient, error) { diff --git a/pkg/registry/apps/alerting/notifications/receiver/conversions.go b/pkg/registry/apps/alerting/notifications/receiver/conversions.go index aca723b7fd7..a6258eeae58 100644 --- a/pkg/registry/apps/alerting/notifications/receiver/conversions.go +++ b/pkg/registry/apps/alerting/notifications/receiver/conversions.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/types" - model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" diff --git a/pkg/registry/apps/alerting/notifications/receiver/legacy_storage.go b/pkg/registry/apps/alerting/notifications/receiver/legacy_storage.go index e224c210ff8..84c0fe9f7fd 100644 --- a/pkg/registry/apps/alerting/notifications/receiver/legacy_storage.go +++ b/pkg/registry/apps/alerting/notifications/receiver/legacy_storage.go @@ -11,7 +11,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" diff --git a/pkg/registry/apps/alerting/notifications/receiver/type.go b/pkg/registry/apps/alerting/notifications/receiver/type.go index 917072fb4a3..b9b0f721bd9 100644 --- a/pkg/registry/apps/alerting/notifications/receiver/type.go +++ b/pkg/registry/apps/alerting/notifications/receiver/type.go @@ -7,7 +7,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" ) diff --git a/pkg/registry/apps/alerting/notifications/register.go b/pkg/registry/apps/alerting/notifications/register.go index 109c714c443..fbf75d44bb5 100644 --- a/pkg/registry/apps/alerting/notifications/register.go +++ b/pkg/registry/apps/alerting/notifications/register.go @@ -4,21 +4,21 @@ import ( "context" "github.com/grafana/grafana-app-sdk/app" + appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" "github.com/grafana/grafana-app-sdk/simple" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/authorization/authorizer" + restclient "k8s.io/client-go/rest" - notificationsResource "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis" - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis" notificationsApp "github.com/grafana/grafana/apps/alerting/notifications/pkg/app" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/receiver" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/routingtree" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/templategroup" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/timeinterval" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/apiserver/builder" - "github.com/grafana/grafana/pkg/services/apiserver/builder/runner" + "github.com/grafana/grafana/pkg/services/apiserver/appinstaller" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/ngalert" ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" @@ -26,31 +26,53 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -type AlertingNotificationsAppProvider struct { - app.Provider +var ( + _ appsdkapiserver.AppInstaller = (*AlertingNotificationsAppInstaller)(nil) + _ appinstaller.LegacyStorageProvider = (*AlertingNotificationsAppInstaller)(nil) + _ appinstaller.AuthorizerProvider = (*AlertingNotificationsAppInstaller)(nil) +) + +type AlertingNotificationsAppInstaller struct { + appsdkapiserver.AppInstaller + cfg *setting.Cfg + ng *ngalert.AlertNG } -func RegisterApp( +func RegisterAppInstaller( cfg *setting.Cfg, ng *ngalert.AlertNG, -) *AlertingNotificationsAppProvider { +) (*AlertingNotificationsAppInstaller, error) { if ng.IsDisabled() { - return nil - } - appCfg := &runner.AppBuilderConfig{ - Authorizer: getAuthorizer(ng.Api.AccessControl), - LegacyStorageGetter: getLegacyStorage(request.GetNamespaceMapper(cfg), ng), - OpenAPIDefGetter: v0alpha1.GetOpenAPIDefinitions, - ManagedKinds: notificationsResource.GetKinds(), - AllowedV0Alpha1Resources: []string{builder.AllResourcesAllowed}, + log.New("app-registry").Info("Skipping Kubernetes Alerting Notifications API server (notifications.alerting.grafana.app): Unified Alerting is disabled") + return nil, nil } - return &AlertingNotificationsAppProvider{ - Provider: simple.NewAppProvider(notificationsResource.LocalManifest(), appCfg, notificationsApp.New), + installer := &AlertingNotificationsAppInstaller{ + cfg: cfg, + ng: ng, } + + localManifest := apis.LocalManifest() + + provider := simple.NewAppProvider(localManifest, nil, notificationsApp.New) + + appConfig := app.Config{ + KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method + ManifestData: *localManifest.ManifestData, + SpecificConfig: nil, + } + + i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, &apis.GoTypeAssociator{}) + if err != nil { + return nil, err + } + installer.AppInstaller = i + + return installer, nil } -func getAuthorizer(authz accesscontrol.AccessControl) authorizer.Authorizer { +func (a AlertingNotificationsAppInstaller) GetAuthorizer() authorizer.Authorizer { + authz := a.ng.Api.AccessControl return authorizer.AuthorizerFunc( func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { switch a.GetResource() { @@ -67,17 +89,17 @@ func getAuthorizer(authz accesscontrol.AccessControl) authorizer.Authorizer { }) } -func getLegacyStorage(namespacer request.NamespaceMapper, ng *ngalert.AlertNG) runner.LegacyStorageGetter { - return func(gvr schema.GroupVersionResource) grafanarest.Storage { - if gvr == receiver.ResourceInfo.GroupVersionResource() { - return receiver.NewStorage(ng.Api.ReceiverService, namespacer, ng.Api.ReceiverService) - } else if gvr == timeinterval.ResourceInfo.GroupVersionResource() { - return timeinterval.NewStorage(ng.Api.MuteTimings, namespacer) - } else if gvr == templategroup.ResourceInfo.GroupVersionResource() { - return templategroup.NewStorage(ng.Api.Templates, namespacer) - } else if gvr == routingtree.ResourceInfo.GroupVersionResource() { - return routingtree.NewStorage(ng.Api.Policies, namespacer) - } - panic("unknown legacy storage requested: " + gvr.String()) +func (a AlertingNotificationsAppInstaller) GetLegacyStorage(gvr schema.GroupVersionResource) grafanarest.Storage { + namespacer := request.GetNamespaceMapper(a.cfg) + api := a.ng.Api + if gvr == receiver.ResourceInfo.GroupVersionResource() { + return receiver.NewStorage(api.ReceiverService, namespacer, api.ReceiverService) + } else if gvr == timeinterval.ResourceInfo.GroupVersionResource() { + return timeinterval.NewStorage(api.MuteTimings, namespacer) + } else if gvr == templategroup.ResourceInfo.GroupVersionResource() { + return templategroup.NewStorage(api.Templates, namespacer) + } else if gvr == routingtree.ResourceInfo.GroupVersionResource() { + return routingtree.NewStorage(api.Policies, namespacer) } + panic("unknown legacy storage requested: " + gvr.String()) } diff --git a/pkg/registry/apps/alerting/notifications/routingtree/conversions.go b/pkg/registry/apps/alerting/notifications/routingtree/conversions.go index a8cc95be44c..70c6ee5ba0d 100644 --- a/pkg/registry/apps/alerting/notifications/routingtree/conversions.go +++ b/pkg/registry/apps/alerting/notifications/routingtree/conversions.go @@ -12,7 +12,7 @@ import ( promModel "github.com/prometheus/common/model" - model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" diff --git a/pkg/registry/apps/alerting/notifications/routingtree/legacy_storage.go b/pkg/registry/apps/alerting/notifications/routingtree/legacy_storage.go index 2f326a43d9c..115c33434e0 100644 --- a/pkg/registry/apps/alerting/notifications/routingtree/legacy_storage.go +++ b/pkg/registry/apps/alerting/notifications/routingtree/legacy_storage.go @@ -10,7 +10,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" diff --git a/pkg/registry/apps/alerting/notifications/routingtree/type.go b/pkg/registry/apps/alerting/notifications/routingtree/type.go index 75059b84ff2..3b0e2b4df3a 100644 --- a/pkg/registry/apps/alerting/notifications/routingtree/type.go +++ b/pkg/registry/apps/alerting/notifications/routingtree/type.go @@ -5,7 +5,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" - model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" ) diff --git a/pkg/registry/apps/alerting/notifications/templategroup/conversions.go b/pkg/registry/apps/alerting/notifications/templategroup/conversions.go index be68e3f0be4..95cd2399816 100644 --- a/pkg/registry/apps/alerting/notifications/templategroup/conversions.go +++ b/pkg/registry/apps/alerting/notifications/templategroup/conversions.go @@ -5,7 +5,7 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/types" - model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" diff --git a/pkg/registry/apps/alerting/notifications/templategroup/legacy_storage.go b/pkg/registry/apps/alerting/notifications/templategroup/legacy_storage.go index f6734bbd356..caf2558da96 100644 --- a/pkg/registry/apps/alerting/notifications/templategroup/legacy_storage.go +++ b/pkg/registry/apps/alerting/notifications/templategroup/legacy_storage.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/alerting/templates" - model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" diff --git a/pkg/registry/apps/alerting/notifications/templategroup/type.go b/pkg/registry/apps/alerting/notifications/templategroup/type.go index 4c71c8fd7d9..74f068827bf 100644 --- a/pkg/registry/apps/alerting/notifications/templategroup/type.go +++ b/pkg/registry/apps/alerting/notifications/templategroup/type.go @@ -7,7 +7,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" ) diff --git a/pkg/registry/apps/alerting/notifications/timeinterval/conversions.go b/pkg/registry/apps/alerting/notifications/timeinterval/conversions.go index ed9edc162c1..bc3682ccedd 100644 --- a/pkg/registry/apps/alerting/notifications/timeinterval/conversions.go +++ b/pkg/registry/apps/alerting/notifications/timeinterval/conversions.go @@ -7,7 +7,7 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/types" - model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" diff --git a/pkg/registry/apps/alerting/notifications/timeinterval/legacy_storage.go b/pkg/registry/apps/alerting/notifications/timeinterval/legacy_storage.go index a76e79f821b..a4cf8cfb387 100644 --- a/pkg/registry/apps/alerting/notifications/timeinterval/legacy_storage.go +++ b/pkg/registry/apps/alerting/notifications/timeinterval/legacy_storage.go @@ -10,7 +10,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" diff --git a/pkg/registry/apps/alerting/notifications/timeinterval/type.go b/pkg/registry/apps/alerting/notifications/timeinterval/type.go index 3e9a958594c..b4a5986702b 100644 --- a/pkg/registry/apps/alerting/notifications/timeinterval/type.go +++ b/pkg/registry/apps/alerting/notifications/timeinterval/type.go @@ -7,7 +7,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" ) diff --git a/pkg/registry/apps/apps.go b/pkg/registry/apps/apps.go index a6e3d51ad83..d7d119235b0 100644 --- a/pkg/registry/apps/apps.go +++ b/pkg/registry/apps/apps.go @@ -35,6 +35,7 @@ func ProvideAppInstallers( shorturlAppInstaller *shorturl.ShortURLAppInstaller, rulesAppInstaller *rules.AlertingRulesAppInstaller, correlationsAppInstaller *correlations.CorrelationsAppInstaller, + alertingNotificationAppInstaller *notifications.AlertingNotificationsAppInstaller, ) []appsdkapiserver.AppInstaller { installers := []appsdkapiserver.AppInstaller{ playlistAppInstaller, @@ -49,6 +50,9 @@ func ProvideAppInstallers( if features.IsEnabledGlobally(featuremgmt.FlagKubernetesCorrelations) { installers = append(installers, correlationsAppInstaller) } + if alertingNotificationAppInstaller != nil { + installers = append(installers, alertingNotificationAppInstaller) + } return installers } @@ -69,7 +73,6 @@ func ProvideBuilderRunners( features featuremgmt.FeatureToggles, investigationAppProvider *investigations.InvestigationsAppProvider, advisorAppProvider *advisor.AdvisorAppProvider, - alertingNotificationsAppProvider *notifications.AlertingNotificationsAppProvider, grafanaCfg *setting.Cfg, ) (*Service, error) { cfgWrapper := func(ctx context.Context) (*rest.Config, error) { @@ -97,9 +100,6 @@ func ProvideBuilderRunners( !slices.Contains(grafanaCfg.DisablePlugins, "grafana-advisor-app") { providers = append(providers, advisorAppProvider) } - if alertingNotificationsAppProvider != nil { - providers = append(providers, alertingNotificationsAppProvider) - } apiGroupRunner, err = runner.NewAPIGroupRunner(cfg, providers...) if err != nil { diff --git a/pkg/registry/apps/apps_test.go b/pkg/registry/apps/apps_test.go index f776f3dabe6..9e6797ac164 100644 --- a/pkg/registry/apps/apps_test.go +++ b/pkg/registry/apps/apps_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apps/alerting/rules" "github.com/grafana/grafana/pkg/registry/apps/correlations" "github.com/grafana/grafana/pkg/registry/apps/playlist" @@ -17,6 +18,7 @@ func TestProvideAppInstallers_Table(t *testing.T) { pluginsInstaller := &plugins.PluginsAppInstaller{} rulesInstaller := &rules.AlertingRulesAppInstaller{} correlationsAppInstaller := &correlations.CorrelationsAppInstaller{} + notificationsAppInstaller := ¬ifications.AlertingNotificationsAppInstaller{} tests := []struct { name string @@ -33,7 +35,7 @@ func TestProvideAppInstallers_Table(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { features := featuremgmt.WithFeatures(tt.flags...) - got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller) + got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller, notificationsAppInstaller) if tt.expectRulesApp { require.Contains(t, got, tt.rulesInst) } else { diff --git a/pkg/registry/apps/wireset.go b/pkg/registry/apps/wireset.go index 11b4142bfdc..b864eef274d 100644 --- a/pkg/registry/apps/wireset.go +++ b/pkg/registry/apps/wireset.go @@ -19,9 +19,9 @@ var WireSet = wire.NewSet( playlist.RegisterAppInstaller, investigations.RegisterApp, advisor.RegisterApp, - notifications.RegisterApp, plugins.RegisterAppInstaller, shorturl.RegisterAppInstaller, correlations.RegisterAppInstaller, rules.RegisterAppInstaller, + notifications.RegisterAppInstaller, ) diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index f0f9de53f93..11a6de3b807 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -644,8 +644,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api qsDatasourceClientBuilder := dsquerierclient.NewNullQSDatasourceClientBuilder() exprService := expr.ProvideService(cfg, middlewareHandler, plugincontextProvider, featureToggles, registerer, tracingService, qsDatasourceClientBuilder) queryServiceImpl := query.ProvideService(cfg, cacheServiceImpl, exprService, ossDataSourceRequestValidator, middlewareHandler, plugincontextProvider, qsDatasourceClientBuilder) - repositoryImpl := annotationsimpl.ProvideService(sqlStore, cfg, featureToggles, tagimplService, tracingService, dBstore, dashboardService, registerer) - grafanaLive, err := live.ProvideService(plugincontextProvider, cfg, routeRegisterImpl, pluginstoreService, middlewareHandler, cacheService, cacheServiceImpl, sqlStore, secretsService, usageStats, queryServiceImpl, featureToggles, accessControl, dashboardService, repositoryImpl, orgService, eventualRestConfigProvider) + grafanaLive, err := live.ProvideService(plugincontextProvider, cfg, routeRegisterImpl, pluginstoreService, middlewareHandler, cacheService, cacheServiceImpl, sqlStore, secretsService, usageStats, queryServiceImpl, featureToggles, accessControl, dashboardService, orgService, eventualRestConfigProvider) if err != nil { return nil, err } @@ -655,6 +654,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api contexthandlerContextHandler := contexthandler.ProvideService(cfg, authnAuthenticator, featureToggles) logger := loggermw.Provide(cfg, featureToggles) ngAlert := metrics2.ProvideService() + repositoryImpl := annotationsimpl.ProvideService(sqlStore, cfg, featureToggles, tagimplService, tracingService, dBstore, dashboardService, registerer) alertNG, err := ngalert.ProvideService(cfg, featureToggles, cacheServiceImpl, service15, routeRegisterImpl, sqlStore, kvStore, exprService, dataSourceProxyService, quotaService, secretsService, notificationService, ngAlert, folderimplService, accessControl, dashboardService, renderingService, inProcBus, acimplService, repositoryImpl, pluginstoreService, tracingService, dBstore, httpclientProvider, plugincontextProvider, receiverPermissionsService, userService) if err != nil { return nil, err @@ -768,7 +768,11 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller) + alertingNotificationsAppInstaller, err := notifications2.RegisterAppInstaller(cfg, alertNG) + if err != nil { + return nil, err + } + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller) builderMetrics := builder.ProvideBuilderMetrics(registerer) apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics) if err != nil { @@ -786,8 +790,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api investigationsAppProvider := investigations.RegisterApp(cfg) checkregistryService := checkregistry.ProvideService(service15, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, managedpluginsNoop, noop, ssosettingsimplService, cfg, pluginerrsStore) advisorAppProvider := advisor2.RegisterApp(checkregistryService, cfg) - alertingNotificationsAppProvider := notifications2.RegisterApp(cfg, alertNG) - appregistryService, err := appregistry.ProvideBuilderRunners(apiserverService, eventualRestConfigProvider, featureToggles, investigationsAppProvider, advisorAppProvider, alertingNotificationsAppProvider, cfg) + appregistryService, err := appregistry.ProvideBuilderRunners(apiserverService, eventualRestConfigProvider, featureToggles, investigationsAppProvider, advisorAppProvider, cfg) if err != nil { return nil, err } @@ -827,9 +830,9 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, starService, userService, apiserverService) legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles) - webhookExtraBuilder := webhooks.ProvideWebhooksWithImages(cfg, renderingService, resourceClient, eventualRestConfigProvider) + webhookExtraBuilder := webhooks.ProvideWebhooksWithImages(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer) v3 := extras.ProvideProvisioningExtraAPIs(webhookExtraBuilder) - pullRequestWorker := pullrequest.ProvidePullRequestWorker(cfg, renderingService, resourceClient, eventualRestConfigProvider) + pullRequestWorker := pullrequest.ProvidePullRequestWorker(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer) v4 := extras.ProvideExtraWorkers(pullRequestWorker) v5 := _wireValue decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, v5) @@ -1248,8 +1251,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac qsDatasourceClientBuilder := dsquerierclient.NewNullQSDatasourceClientBuilder() exprService := expr.ProvideService(cfg, middlewareHandler, plugincontextProvider, featureToggles, registerer, tracingService, qsDatasourceClientBuilder) queryServiceImpl := query.ProvideService(cfg, cacheServiceImpl, exprService, ossDataSourceRequestValidator, middlewareHandler, plugincontextProvider, qsDatasourceClientBuilder) - repositoryImpl := annotationsimpl.ProvideService(sqlStore, cfg, featureToggles, tagimplService, tracingService, dBstore, dashboardService, registerer) - grafanaLive, err := live.ProvideService(plugincontextProvider, cfg, routeRegisterImpl, pluginstoreService, middlewareHandler, cacheService, cacheServiceImpl, sqlStore, secretsService, usageStats, queryServiceImpl, featureToggles, accessControl, dashboardService, repositoryImpl, orgService, eventualRestConfigProvider) + grafanaLive, err := live.ProvideService(plugincontextProvider, cfg, routeRegisterImpl, pluginstoreService, middlewareHandler, cacheService, cacheServiceImpl, sqlStore, secretsService, usageStats, queryServiceImpl, featureToggles, accessControl, dashboardService, orgService, eventualRestConfigProvider) if err != nil { return nil, err } @@ -1260,6 +1262,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac logger := loggermw.Provide(cfg, featureToggles) notificationServiceMock := notifications.MockNotificationService() ngAlert := metrics2.ProvideServiceForTest() + repositoryImpl := annotationsimpl.ProvideService(sqlStore, cfg, featureToggles, tagimplService, tracingService, dBstore, dashboardService, registerer) alertNG, err := ngalert.ProvideService(cfg, featureToggles, cacheServiceImpl, service15, routeRegisterImpl, sqlStore, kvStore, exprService, dataSourceProxyService, quotaService, secretsService, notificationServiceMock, ngAlert, folderimplService, accessControl, dashboardService, renderingService, inProcBus, acimplService, repositoryImpl, pluginstoreService, tracingService, dBstore, httpclientProvider, plugincontextProvider, receiverPermissionsService, userService) if err != nil { return nil, err @@ -1373,7 +1376,11 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller) + alertingNotificationsAppInstaller, err := notifications2.RegisterAppInstaller(cfg, alertNG) + if err != nil { + return nil, err + } + v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller) builderMetrics := builder.ProvideBuilderMetrics(registerer) apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics) if err != nil { @@ -1391,8 +1398,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac investigationsAppProvider := investigations.RegisterApp(cfg) checkregistryService := checkregistry.ProvideService(service15, pluginstoreService, plugincontextProvider, middlewareHandler, plugincheckerService, repoManager, preinstallImpl, managedpluginsNoop, noop, ssosettingsimplService, cfg, pluginerrsStore) advisorAppProvider := advisor2.RegisterApp(checkregistryService, cfg) - alertingNotificationsAppProvider := notifications2.RegisterApp(cfg, alertNG) - appregistryService, err := appregistry.ProvideBuilderRunners(apiserverService, eventualRestConfigProvider, featureToggles, investigationsAppProvider, advisorAppProvider, alertingNotificationsAppProvider, cfg) + appregistryService, err := appregistry.ProvideBuilderRunners(apiserverService, eventualRestConfigProvider, featureToggles, investigationsAppProvider, advisorAppProvider, cfg) if err != nil { return nil, err } @@ -1432,9 +1438,9 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, starService, userService, apiserverService) legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles) - webhookExtraBuilder := webhooks.ProvideWebhooksWithImages(cfg, renderingService, resourceClient, eventualRestConfigProvider) + webhookExtraBuilder := webhooks.ProvideWebhooksWithImages(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer) v3 := extras.ProvideProvisioningExtraAPIs(webhookExtraBuilder) - pullRequestWorker := pullrequest.ProvidePullRequestWorker(cfg, renderingService, resourceClient, eventualRestConfigProvider) + pullRequestWorker := pullrequest.ProvidePullRequestWorker(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer) v4 := extras.ProvideExtraWorkers(pullRequestWorker) v5 := _wireValue decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, v5) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 8cfb9d5121b..ad6894feb44 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -392,6 +392,7 @@ const ( // Settings scope ScopeSettingsAll = "settings:*" ScopeSettingsSAML = "settings:auth.saml:*" + ScopeSettingsSCIM = "settings:auth.scim:*" // Team related actions ActionTeamsCreate = "teams:create" diff --git a/pkg/services/accesscontrol/roles.go b/pkg/services/accesscontrol/roles.go index 6ed0357d9de..6d01e3b8624 100644 --- a/pkg/services/accesscontrol/roles.go +++ b/pkg/services/accesscontrol/roles.go @@ -273,6 +273,14 @@ var ( Action: ActionSettingsWrite, Scope: ScopeSettingsOAuth("ldap"), }, + { + Action: ActionSettingsRead, + Scope: ScopeSettingsSCIM, + }, + { + Action: ActionSettingsWrite, + Scope: ScopeSettingsSCIM, + }, }, } diff --git a/pkg/services/apiserver/appinstaller/server.go b/pkg/services/apiserver/appinstaller/server.go index 0a8ce439fd4..b6b32be48e9 100644 --- a/pkg/services/apiserver/appinstaller/server.go +++ b/pkg/services/apiserver/appinstaller/server.go @@ -120,5 +120,8 @@ func (s *serverWrapper) configureStorage(gr schema.GroupResource, dualWriteSuppo } func (s *serverWrapper) RegisteredWebServices() []*restful.WebService { - return []*restful.WebService{} + if s.Handler != nil && s.Handler.GoRestfulContainer != nil { + return s.Handler.GoRestfulContainer.RegisteredWebServices() + } + return nil } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 5925165fbf7..81d2f4b5c9f 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -171,6 +171,15 @@ var ( Expression: "true", // enabled by default AllowSelfServe: false, }, + { + Name: "starsFromAPIServer", + Description: "populate star status from apiserver", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaFrontendPlatformSquad, + AllowSelfServe: false, + HideFromDocs: true, + }, { Name: "influxqlStreamingParser", Description: "Enable streaming JSON parser for InfluxDB datasource InfluxQL query language", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 9ae74d0f547..dfde79183c8 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -20,6 +20,7 @@ lokiShardSplitting,experimental,@grafana/observability-logs,false,false,true lokiQuerySplitting,GA,@grafana/observability-logs,false,false,true individualCookiePreferences,experimental,@grafana/grafana-backend-group,false,false,false influxdbBackendMigration,GA,@grafana/partner-datasources,false,false,true +starsFromAPIServer,experimental,@grafana/grafana-frontend-platform,false,false,true influxqlStreamingParser,experimental,@grafana/partner-datasources,false,false,false influxdbRunQueriesInParallel,privatePreview,@grafana/partner-datasources,false,false,false lokiLogsDataplane,experimental,@grafana/observability-logs,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 56f4e6ca9ec..913a0ac38db 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -91,6 +91,10 @@ const ( // Query InfluxDB InfluxQL without the proxy FlagInfluxdbBackendMigration = "influxdbBackendMigration" + // FlagStarsFromAPIServer + // populate star status from apiserver + FlagStarsFromAPIServer = "starsFromAPIServer" + // FlagInfluxqlStreamingParser // Enable streaming JSON parser for InfluxDB datasource InfluxQL query language FlagInfluxqlStreamingParser = "influxqlStreamingParser" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 9d04a7e9a30..610ffc59594 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3356,6 +3356,20 @@ "expression": "true" } }, + { + "metadata": { + "name": "starsFromAPIServer", + "resourceVersion": "1758276055065", + "creationTimestamp": "2025-09-19T10:00:55Z" + }, + "spec": { + "description": "populate star status from apiserver", + "stage": "experimental", + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "storage", diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index 0394e06fdc0..d3648d12f5c 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -25,7 +25,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/live" - "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -39,7 +38,6 @@ import ( "github.com/grafana/grafana/pkg/middleware/requestmeta" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/apiserver" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" @@ -84,7 +82,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r pluginStore pluginstore.Store, pluginClient plugins.Client, cacheService *localcache.CacheService, dataSourceCache datasources.CacheService, sqlStore db.DB, secretsService secrets.Service, usageStatsService usagestats.Service, queryDataService query.Service, toggles featuremgmt.FeatureToggles, - accessControl accesscontrol.AccessControl, dashboardService dashboards.DashboardService, annotationsRepo annotations.Repository, + accessControl accesscontrol.AccessControl, dashboardService dashboards.DashboardService, orgService org.Service, configProvider apiserver.RestConfigProvider) (*GrafanaLive, error) { g := &GrafanaLive{ Cfg: cfg, diff --git a/pkg/services/live/live_test.go b/pkg/services/live/live_test.go index cfead7e50ec..00ce4bee3f4 100644 --- a/pkg/services/live/live_test.go +++ b/pkg/services/live/live_test.go @@ -22,7 +22,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" - "github.com/grafana/grafana/pkg/services/annotations/annotationstest" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/live/livecontext" @@ -355,7 +354,6 @@ func setupLiveService(cfg *setting.Cfg, t *testing.T) (*GrafanaLive, error) { featuremgmt.WithFeatures(), acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), &dashboards.FakeDashboardService{}, - annotationstest.NewFakeAnnotationsRepo(), nil, nil) } diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 0cc24694b96..e009b7bc196 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -75,6 +75,35 @@ func (s *ServiceImpl) addAppLinks(treeRoot *navtree.NavTreeRoot, c *contextmodel return nil } +// shouldIncludeInvestigations checks if the investigations feature should be included for the assistant app +// see https://github.com/grafana/grafana-assistant-app/issues/2007 for more details +func (s *ServiceImpl) shouldIncludeInvestigations(plugin pluginstore.Plugin, include *plugins.Includes, c *contextmodel.ReqContext) bool { + if plugin.ID != "grafana-assistant-app" || include.Name != "Investigations" { + return true + } + + ps, err := s.pluginSettings.GetPluginSettingByPluginID(c.Req.Context(), &pluginsettings.GetByPluginIDArgs{ + PluginID: plugin.ID, + OrgID: c.GetOrgID(), + }) + if err != nil { + return false + } + + loopData, exists := ps.JSONData["loop"] + if !exists { + return false + } + + loopConfig, ok := loopData.(map[string]any) + if !ok { + return false + } + + enabled, ok := loopConfig["enabled"].(bool) + return ok && enabled +} + func (s *ServiceImpl) processAppPlugin(plugin pluginstore.Plugin, c *contextmodel.ReqContext, treeRoot *navtree.NavTreeRoot) *navtree.NavLink { hasAccessToInclude := s.hasAccessToInclude(c, plugin.ID) appLink := &navtree.NavLink{ @@ -93,6 +122,10 @@ func (s *ServiceImpl) processAppPlugin(plugin pluginstore.Plugin, c *contextmode continue } + if !s.shouldIncludeInvestigations(plugin, include, c) { + continue + } + if include.Type == "page" { link := &navtree.NavLink{ Text: include.Name, diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index f31b993ad9d..137c18f031c 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -175,11 +175,11 @@ func (h *RemoteLokiBackend) Query(ctx context.Context, query models.HistoryQuery } res = append(res, r.Data.Result...) } - return merge(res, uids) + return h.merge(res, uids) } // merge will put all the results in one array sorted by timestamp. -func merge(res []lokiclient.Stream, folderUIDToFilter []string) (*data.Frame, error) { +func (h RemoteLokiBackend) merge(res []lokiclient.Stream, folderUIDToFilter []string) (*data.Frame, error) { filterByFolderUIDMap := make(map[string]struct{}, len(folderUIDToFilter)) for _, uid := range folderUIDToFilter { filterByFolderUIDMap[uid] = struct{}{} @@ -241,27 +241,27 @@ func merge(res []lokiclient.Stream, folderUIDToFilter []string) (*data.Frame, er if minElStreamIdx == -1 { break } + entryBytes := []byte(minEl.V) var entry LokiEntry - err := json.Unmarshal([]byte(minEl.V), &entry) + err := json.Unmarshal(entryBytes, &entry) if err != nil { - return nil, fmt.Errorf("failed to unmarshal entry: %w", err) + h.log.Warn("failed to unmarshal entry, continuing", "err", err, "entry", minEl.V) + pointers[minElStreamIdx]++ + continue } // Append the minimum element to the merged slice and move the pointer. tsNano := minEl.T.UnixNano() - // TODO: In general, perhaps we should omit the offending line and log, rather than failing the request entirely. streamLbls := res[minElStreamIdx].Stream lblsJson, err := json.Marshal(streamLbls) if err != nil { - return nil, fmt.Errorf("failed to serialize stream labels: %w", err) + // This should in theory never happen, as we're marshalling a map[string]string. + h.log.Warn("failed to serialize stream labels, continuing", "err", err, "labels", streamLbls) + pointers[minElStreamIdx]++ + continue } - line, err := jsonifyRow(minEl.V) - if err != nil { - return nil, fmt.Errorf("a line was in an invalid format: %w", err) - } - times = append(times, time.Unix(0, tsNano)) labels = append(labels, lblsJson) - lines = append(lines, line) + lines = append(lines, json.RawMessage(entryBytes)) pointers[minElStreamIdx]++ } @@ -359,17 +359,6 @@ func valuesAsDataBlob(state *state.State) *simplejson.Json { return jsonifyValues(state.Values) } -func jsonifyRow(line string) (json.RawMessage, error) { - // Ser/deser to validate the contents of the log line before shipping it forward. - // TODO: We may want to remove this in the future, as we already have the value in the form of a []byte, and json.RawMessage is also a []byte. - // TODO: Though, if the log line does not contain valid JSON, this can cause problems later on when rendering the dataframe. - var entry LokiEntry - if err := json.Unmarshal([]byte(line), &entry); err != nil { - return nil, err - } - return json.Marshal(entry) -} - // BuildLogQuery converts models.HistoryQuery and a list of folder UIDs to Loki queries. // It can return multiple queries if the list of folder UIDs is too big to fit into single query. // If there is a folder UID long enough to exceed a query size it returns ErrQueryTooLong. diff --git a/pkg/services/ngalert/state/historian/loki_test.go b/pkg/services/ngalert/state/historian/loki_test.go index 99d9aff3b79..a01efe9563b 100644 --- a/pkg/services/ngalert/state/historian/loki_test.go +++ b/pkg/services/ngalert/state/historian/loki_test.go @@ -411,7 +411,7 @@ func TestMerge(t *testing.T) { "extra": "label", }, Values: []lokiclient.Sample{ - {T: time.Unix(1, 0), V: `{"schemaVersion": 1, "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1"}`}, + {T: time.Unix(1, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1", "ruleID": 123}`}, }, }, { @@ -422,7 +422,7 @@ func TestMerge(t *testing.T) { "folderUID": "test-folder-1", }, Values: []lokiclient.Sample{ - {T: time.Unix(2, 0), V: `{"schemaVersion": 1, "previous": "pending", "current": "firing", "values":{"a": 2.5}, "ruleUID": "test-rule-2"}`}, + {T: time.Unix(2, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "pending", "current": "firing", "values":{"a": 2.5}, "ruleUID": "test-rule-2", "ruleID": 123}`}, }, }, }, @@ -434,8 +434,8 @@ func TestMerge(t *testing.T) { time.Unix(2, 0), }), data.NewField(dfLine, data.Labels{}, []json.RawMessage{ - toJson(LokiEntry{RuleUID: "test-rule-1", SchemaVersion: 1, Previous: "normal", Current: "pending", Values: jsonifyValues(map[string]float64{"a": 1.5})}), - toJson(LokiEntry{RuleUID: "test-rule-2", SchemaVersion: 1, Previous: "pending", Current: "firing", Values: jsonifyValues(map[string]float64{"a": 2.5})}), + toJson(LokiEntry{RuleUID: "test-rule-1", SchemaVersion: 1, Previous: "normal", Current: "pending", Fingerprint: "test", PanelID: 123, RuleTitle: "test", InstanceLabels: map[string]string{}, RuleID: 123, Condition: "test", DashboardUID: "123", Values: jsonifyValues(map[string]float64{"a": 1.5})}), + toJson(LokiEntry{RuleUID: "test-rule-2", SchemaVersion: 1, Previous: "pending", Current: "firing", Fingerprint: "test", PanelID: 123, RuleTitle: "test", InstanceLabels: map[string]string{}, RuleID: 123, Condition: "test", DashboardUID: "123", Values: jsonifyValues(map[string]float64{"a": 2.5})}), }), data.NewField(dfLabels, data.Labels{}, []json.RawMessage{ toJson(map[string]string{ @@ -487,8 +487,8 @@ func TestMerge(t *testing.T) { "folderUID": "test-folder-1", }, Values: []lokiclient.Sample{ - {T: time.Unix(1, 0), V: `{"schemaVersion": 1, "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1"}`}, - {T: time.Unix(5, 0), V: `{"schemaVersion": 1, "previous": "pending", "current": "normal", "values":{"a": 0.5}, "ruleUID": "test-rule-2"}`}, + {T: time.Unix(1, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1", "ruleID": 123}`}, + {T: time.Unix(5, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "pending", "current": "normal", "values":{"a": 0.5}, "ruleUID": "test-rule-2", "ruleID": 123}`}, }, }, { @@ -499,7 +499,7 @@ func TestMerge(t *testing.T) { "folderUID": "test-folder-1", }, Values: []lokiclient.Sample{ - {T: time.Unix(2, 0), V: `{"schemaVersion": 1, "previous": "pending", "current": "firing", "values":{"a": 2.5}, "ruleUID": "test-rule-3"}`}, + {T: time.Unix(2, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "pending", "current": "firing", "values":{"a": 2.5}, "ruleUID": "test-rule-3", "ruleID": 123}`}, }, }, }, @@ -512,9 +512,9 @@ func TestMerge(t *testing.T) { time.Unix(5, 0), }), data.NewField(dfLine, data.Labels{}, []json.RawMessage{ - toJson(LokiEntry{RuleUID: "test-rule-1", SchemaVersion: 1, Previous: "normal", Current: "pending", Values: jsonifyValues(map[string]float64{"a": 1.5})}), - toJson(LokiEntry{RuleUID: "test-rule-3", SchemaVersion: 1, Previous: "pending", Current: "firing", Values: jsonifyValues(map[string]float64{"a": 2.5})}), - toJson(LokiEntry{RuleUID: "test-rule-2", SchemaVersion: 1, Previous: "pending", Current: "normal", Values: jsonifyValues(map[string]float64{"a": 0.5})}), + toJson(LokiEntry{RuleUID: "test-rule-1", SchemaVersion: 1, Previous: "normal", Current: "pending", Fingerprint: "test", PanelID: 123, RuleTitle: "test", InstanceLabels: map[string]string{}, RuleID: 123, Condition: "test", DashboardUID: "123", Values: jsonifyValues(map[string]float64{"a": 1.5})}), + toJson(LokiEntry{RuleUID: "test-rule-3", SchemaVersion: 1, Previous: "pending", Current: "firing", Fingerprint: "test", PanelID: 123, RuleTitle: "test", InstanceLabels: map[string]string{}, RuleID: 123, Condition: "test", DashboardUID: "123", Values: jsonifyValues(map[string]float64{"a": 2.5})}), + toJson(LokiEntry{RuleUID: "test-rule-2", SchemaVersion: 1, Previous: "pending", Current: "normal", Fingerprint: "test", PanelID: 123, RuleTitle: "test", InstanceLabels: map[string]string{}, RuleID: 123, Condition: "test", DashboardUID: "123", Values: jsonifyValues(map[string]float64{"a": 0.5})}), }), data.NewField(dfLabels, data.Labels{}, []json.RawMessage{ toJson(map[string]string{ @@ -538,6 +538,62 @@ func TestMerge(t *testing.T) { }), ), }, + { + name: "Should handle bad values", + res: lokiclient.QueryRes{ + Data: lokiclient.QueryData{ + Result: []lokiclient.Stream{ + { + Stream: map[string]string{ + "from": "state-history", + "orgID": "1", + "group": "test-group-1", + "folderUID": "test-folder-1", + }, + Values: []lokiclient.Sample{ + {T: time.Unix(1, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1", "ruleID": 123}`}, + {T: time.Unix(5, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "pending", "current": "normal", "values":{"a": 0.5}, "ruleUID": "test-rule-2", "ruleID": 123, "bad_label": "\e"}`}, + }, + }, + { + Stream: map[string]string{ + "from": "state-history", + "orgID": "1", + "group": "test-group-2", + "folderUID": "test-folder-1", + }, + Values: []lokiclient.Sample{ + {T: time.Unix(2, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "pending", "current": "firing", "values":{"a": 2.5}, "ruleUID": "test-rule-3", "ruleID": 123}`}, + }, + }, + }, + }, + }, + expected: data.NewFrame("states", + data.NewField(dfTime, data.Labels{}, []time.Time{ + time.Unix(1, 0), + time.Unix(2, 0), + }), + data.NewField(dfLine, data.Labels{}, []json.RawMessage{ + toJson(LokiEntry{RuleUID: "test-rule-1", SchemaVersion: 1, Previous: "normal", Current: "pending", Fingerprint: "test", PanelID: 123, RuleTitle: "test", InstanceLabels: map[string]string{}, RuleID: 123, Condition: "test", DashboardUID: "123", Values: jsonifyValues(map[string]float64{"a": 1.5})}), + toJson(LokiEntry{RuleUID: "test-rule-3", SchemaVersion: 1, Previous: "pending", Current: "firing", Fingerprint: "test", PanelID: 123, RuleTitle: "test", InstanceLabels: map[string]string{}, RuleID: 123, Condition: "test", DashboardUID: "123", Values: jsonifyValues(map[string]float64{"a": 2.5})}), + }), + data.NewField(dfLabels, data.Labels{}, []json.RawMessage{ + toJson(map[string]string{ + StateHistoryLabelKey: "state-history", + OrgIDLabel: "1", + GroupLabel: "test-group-1", + FolderUIDLabel: "test-folder-1", + }), + toJson(map[string]string{ + StateHistoryLabelKey: "state-history", + OrgIDLabel: "1", + GroupLabel: "test-group-2", + FolderUIDLabel: "test-folder-1", + }), + }), + ), + }, { name: "should filter streams by folder UID", folderUIDs: []string{"test-folder-1"}, @@ -552,8 +608,8 @@ func TestMerge(t *testing.T) { "folderUID": "test-folder-1", }, Values: []lokiclient.Sample{ - {T: time.Unix(1, 0), V: `{"schemaVersion": 1, "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1"}`}, - {T: time.Unix(5, 0), V: `{"schemaVersion": 1, "previous": "pending", "current": "normal", "values":{"a": 0.5}, "ruleUID": "test-rule-2"}`}, + {T: time.Unix(1, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1", "ruleID": 123}`}, + {T: time.Unix(5, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "pending", "current": "normal", "values":{"a": 0.5}, "ruleUID": "test-rule-2", "ruleID": 123}`}, }, }, { @@ -564,7 +620,7 @@ func TestMerge(t *testing.T) { "folderUID": "test-folder-2", }, Values: []lokiclient.Sample{ - {T: time.Unix(2, 0), V: `{"schemaVersion": 1, "previous": "pending", "current": "firing", "values":{"a": 2.5}, "ruleUID": "test-rule-3"}`}, + {T: time.Unix(2, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "pending", "current": "firing", "values":{"a": 2.5}, "ruleUID": "test-rule-3", "ruleID": 123}`}, }, }, }, @@ -576,8 +632,8 @@ func TestMerge(t *testing.T) { time.Unix(5, 0), }), data.NewField(dfLine, data.Labels{}, []json.RawMessage{ - toJson(LokiEntry{RuleUID: "test-rule-1", SchemaVersion: 1, Previous: "normal", Current: "pending", Values: jsonifyValues(map[string]float64{"a": 1.5})}), - toJson(LokiEntry{RuleUID: "test-rule-2", SchemaVersion: 1, Previous: "pending", Current: "normal", Values: jsonifyValues(map[string]float64{"a": 0.5})}), + toJson(LokiEntry{RuleUID: "test-rule-1", SchemaVersion: 1, Previous: "normal", Current: "pending", Fingerprint: "test", PanelID: 123, RuleTitle: "test", InstanceLabels: map[string]string{}, RuleID: 123, Condition: "test", DashboardUID: "123", Values: jsonifyValues(map[string]float64{"a": 1.5})}), + toJson(LokiEntry{RuleUID: "test-rule-2", SchemaVersion: 1, Previous: "pending", Current: "normal", Fingerprint: "test", PanelID: 123, RuleTitle: "test", InstanceLabels: map[string]string{}, RuleID: 123, Condition: "test", DashboardUID: "123", Values: jsonifyValues(map[string]float64{"a": 0.5})}), }), data.NewField(dfLabels, data.Labels{}, []json.RawMessage{ toJson(map[string]string{ @@ -606,8 +662,8 @@ func TestMerge(t *testing.T) { "group": "test-group-1", }, Values: []lokiclient.Sample{ - {T: time.Unix(1, 0), V: `{"schemaVersion": 1, "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1"}`}, - {T: time.Unix(5, 0), V: `{"schemaVersion": 1, "previous": "pending", "current": "normal", "values":{"a": 0.5}, "ruleUID": "test-rule-2"}`}, + {T: time.Unix(1, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1", "ruleID": 123}`}, + {T: time.Unix(5, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "pending", "current": "normal", "values":{"a": 0.5}, "ruleUID": "test-rule-2", "ruleID": 123}`}, }, }, }, @@ -630,7 +686,7 @@ func TestMerge(t *testing.T) { "group": "test-group-1", }, Values: []lokiclient.Sample{ - {T: time.Unix(1, 0), V: `{"schemaVersion": 1, "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1"}`}, + {T: time.Unix(1, 0), V: `{"schemaVersion": 1, "condition": "test", "dashboardUID": "123", "fingerprint": "test", "labels": {}, "panelID": 123, "ruleTitle": "test", "previous": "normal", "current": "pending", "values":{"a": 1.5}, "ruleUID": "test-rule-1", "ruleID": 123}`}, }, }, }, @@ -641,7 +697,7 @@ func TestMerge(t *testing.T) { time.Unix(1, 0), }), data.NewField(dfLine, data.Labels{}, []json.RawMessage{ - toJson(LokiEntry{RuleUID: "test-rule-1", SchemaVersion: 1, Previous: "normal", Current: "pending", Values: jsonifyValues(map[string]float64{"a": 1.5})}), + toJson(LokiEntry{RuleUID: "test-rule-1", SchemaVersion: 1, Previous: "normal", Current: "pending", Fingerprint: "test", PanelID: 123, RuleTitle: "test", InstanceLabels: map[string]string{}, RuleID: 123, Condition: "test", DashboardUID: "123", Values: jsonifyValues(map[string]float64{"a": 1.5})}), }), data.NewField(dfLabels, data.Labels{}, []json.RawMessage{ toJson(map[string]string{ @@ -654,9 +710,11 @@ func TestMerge(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + req := instrumenttest.NewFakeRequester() + loki := createTestLokiBackend(t, req, metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem)) expectedJson, err := tc.expected.MarshalJSON() require.NoError(t, err) - m, err := merge(tc.res.Data.Result, tc.folderUIDs) + m, err := loki.merge(tc.res.Data.Result, tc.folderUIDs) require.NoError(t, err) actualJson, err := m.MarshalJSON() assert.NoError(t, err) diff --git a/pkg/storage/unified/resource/bulk.go b/pkg/storage/unified/resource/bulk.go index a0720efa6de..90298c0da66 100644 --- a/pkg/storage/unified/resource/bulk.go +++ b/pkg/storage/unified/resource/bulk.go @@ -170,18 +170,6 @@ func (s *server) BulkProcess(stream resourcepb.BulkStore_BulkProcessServer) erro }) } - // Verify all request keys are valid - for _, k := range settings.Collection { - if r := verifyRequestKey(k); r != nil { - return sendAndClose(&resourcepb.BulkResponse{ - Error: &resourcepb.ErrorResult{ - Message: fmt.Sprintf("invalid request key: %s", r.Message), - Code: http.StatusBadRequest, - }, - }) - } - } - if settings.RebuildCollection { for _, k := range settings.Collection { // Can we delete the whole collection diff --git a/pkg/storage/unified/resource/keys.go b/pkg/storage/unified/resource/keys.go index 204246f3497..0af18d6c048 100644 --- a/pkg/storage/unified/resource/keys.go +++ b/pkg/storage/unified/resource/keys.go @@ -17,18 +17,6 @@ func verifyRequestKey(key *resourcepb.ResourceKey) *resourcepb.ErrorResult { if key.Resource == "" { return NewBadRequestError("request key is missing resource") } - if err := validateName(key.Name); err != nil { - return NewBadRequestError(fmt.Sprintf("name '%s' is invalid: '%s'", key.Name, err)) - } - if err := validateQualifiedName(key.Namespace); err != nil { - return NewBadRequestError(fmt.Sprintf("namespace '%s' is invalid: '%s'", key.Namespace, err)) - } - if err := validateQualifiedName(key.Group); err != nil { - return NewBadRequestError(fmt.Sprintf("group '%s' is invalid: '%s'", key.Group, err)) - } - if err := validateQualifiedName(key.Resource); err != nil { - return NewBadRequestError(fmt.Sprintf("resource '%s' is invalid: '%s'", key.Resource, err)) - } return nil } diff --git a/pkg/storage/unified/resource/keys_test.go b/pkg/storage/unified/resource/keys_test.go index f86f1224313..d14c03fcc95 100644 --- a/pkg/storage/unified/resource/keys_test.go +++ b/pkg/storage/unified/resource/keys_test.go @@ -1,8 +1,6 @@ package resource import ( - "net/http" - "strings" "testing" "github.com/stretchr/testify/require" @@ -68,116 +66,3 @@ func TestSearchIDKeys(t *testing.T) { } } } - -func TestVerifyRequestKey(t *testing.T) { - validGroup := "group.grafana.app" - validResource := "resource" - validNamespace := "default" - validName := "fdgsv37qslr0ga" - validLegacyUID := "f8cc010c-ee72-4681-89d2-d46e1bd47d33" - - invalidGroup := "group.~~~~~grafana.app" - invalidResource := "##resource" - invalidNamespace := "(((((default" - invalidName := " " // only spaces - - namespaceTooLong := strings.Repeat("a", MaxQualifiedNameLength+1) - nameTooLong := strings.Repeat("a", 300) - - tests := []struct { - name string - input *resourcepb.ResourceKey - expectedCode int32 - }{ - { - name: "no error when all fields are set and valid", - input: &resourcepb.ResourceKey{ - Namespace: validNamespace, - Group: validGroup, - Resource: validResource, - Name: validName, - }, - }, - { - name: "invalid namespace returns error", - input: &resourcepb.ResourceKey{ - Namespace: invalidNamespace, - Group: validGroup, - Resource: validResource, - Name: validName, - }, - expectedCode: http.StatusBadRequest, - }, - { - name: "invalid group returns error", - input: &resourcepb.ResourceKey{ - Namespace: validNamespace, - Group: invalidGroup, - Resource: validResource, - Name: validName, - }, - expectedCode: http.StatusBadRequest, - }, - { - name: "invalid resource returns error", - input: &resourcepb.ResourceKey{ - Namespace: validNamespace, - Group: validGroup, - Resource: invalidResource, - Name: validName, - }, - expectedCode: http.StatusBadRequest, - }, - { - name: "invalid name returns error", - input: &resourcepb.ResourceKey{ - Namespace: validNamespace, - Group: validGroup, - Resource: validResource, - Name: invalidName, - }, - expectedCode: http.StatusBadRequest, - }, - { - name: "valid legacy UID returns no error", - input: &resourcepb.ResourceKey{ - Namespace: validNamespace, - Group: validGroup, - Resource: validResource, - Name: validLegacyUID, - }, - }, - { - name: "namespace too long returns error", - input: &resourcepb.ResourceKey{ - Namespace: namespaceTooLong, - Group: validGroup, - Resource: validResource, - Name: validName, - }, - expectedCode: http.StatusBadRequest, - }, - { - name: "name too long returns error", - input: &resourcepb.ResourceKey{ - Namespace: namespaceTooLong, - Group: validGroup, - Resource: validResource, - Name: nameTooLong, - }, - expectedCode: http.StatusBadRequest, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - err := verifyRequestKey(test.input) - if test.expectedCode == 0 { - require.Nil(t, err) - return - } - - require.Equal(t, test.expectedCode, err.Code) - }) - } -} diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 55fe947fa56..35987a418b8 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -626,10 +626,6 @@ func (s *server) Create(ctx context.Context, req *resourcepb.CreateRequest) (*re ctx, span := s.tracer.Start(ctx, "storage_server.Create") defer span.End() - if r := verifyRequestKey(req.Key); r != nil { - return nil, fmt.Errorf("invalid request key: %s", r.Message) - } - rsp := &resourcepb.CreateResponse{} user, ok := claims.AuthInfoFrom(ctx) if !ok || user == nil { diff --git a/pkg/storage/unified/resource/server_test.go b/pkg/storage/unified/resource/server_test.go index 150ee328af1..98bc928da63 100644 --- a/pkg/storage/unified/resource/server_test.go +++ b/pkg/storage/unified/resource/server_test.go @@ -7,7 +7,6 @@ import ( "log/slog" "net/http" "os" - "strings" "sync" "testing" "time" @@ -196,202 +195,6 @@ func TestSimpleServer(t *testing.T) { require.Len(t, all.Items, 0) // empty }) - t.Run("playlist FAIL CRUD paths due to invalid key", func(t *testing.T) { - raw := []byte(`{ - "apiVersion": "playlist.grafana.app/v0alpha1", - "kind": "Playlist", - "metadata": { - "name": "fdgsv37#qslr0ga", - "uid": "xyz", - "namespace": "default", - "annotations": { - "grafana.app/repoName": "elsewhere", - "grafana.app/repoPath": "path/to/item", - "grafana.app/repoTimestamp": "2024-02-02T00:00:00Z" - } - }, - "spec": { - "title": "hello", - "interval": "5m", - "items": [ - { - "type": "dashboard_by_uid", - "value": "vmie2cmWz" - } - ] - } - }`) - - // invalid group - key := &resourcepb.ResourceKey{ - Group: "playlist.grafana.app###", - Resource: "rrrr", // can be anything :( - Namespace: "default", - Name: "fdgsv37qslr0ga", - } - - created, err := server.Create(ctx, &resourcepb.CreateRequest{ - Value: raw, - Key: key, - }) - require.Error(t, err) - require.Nil(t, created) - - // invalid resource - key = &resourcepb.ResourceKey{ - Group: "playlist.grafana.app", - Resource: "rrrr###", // can be anything :( - Namespace: "default", - Name: "fdgsv37qslr0ga", - } - - created, err = server.Create(ctx, &resourcepb.CreateRequest{ - Value: raw, - Key: key, - }) - require.Error(t, err) - require.Nil(t, created) - - // invalid namespace - key = &resourcepb.ResourceKey{ - Group: "playlist.grafana.app", - Resource: "rrrr", // can be anything :( - Namespace: "default###", - Name: "fdgsv37qslr0ga", - } - - created, err = server.Create(ctx, &resourcepb.CreateRequest{ - Value: raw, - Key: key, - }) - require.Error(t, err) - require.Nil(t, created) - - // invalid name - key = &resourcepb.ResourceKey{ - Group: "playlist.grafana.app", - Resource: "rrrr", // can be anything :( - Namespace: "default", - Name: "fdgsv37qslr0g###", - } - - created, err = server.Create(ctx, &resourcepb.CreateRequest{ - Value: raw, - Key: key, - }) - require.Error(t, err) - require.Nil(t, created) - - // legacy name - valid - key = &resourcepb.ResourceKey{ - Group: "playlist.grafana.app", - Resource: "rrrr", // can be anything :( - Namespace: "default", - Name: "2c7e5361-7360-4d2a-ae45-5e79bba458d6", - } - - created, err = server.Create(ctx, &resourcepb.CreateRequest{ - Value: raw, - Key: key, - }) - - require.NoError(t, err) - require.NotNil(t, created) - - // legacy name - also valid - key = &resourcepb.ResourceKey{ - Group: "playlist.grafana.app", - Resource: "rrrr", // can be anything :( - Namespace: "default", - Name: "IvIsO_YGz", - } - - created, err = server.Create(ctx, &resourcepb.CreateRequest{ - Value: raw, - Key: key, - }) - - require.NoError(t, err) - require.NotNil(t, created) - - // legacy name - also valid - key = &resourcepb.ResourceKey{ - Group: "playlist.grafana.app", - Resource: "rrrr", // can be anything :( - Namespace: "default", - Name: "_IvIsOYGz", - } - - created, err = server.Create(ctx, &resourcepb.CreateRequest{ - Value: raw, - Key: key, - }) - - require.NoError(t, err) - require.NotNil(t, created) - - invalidQualifiedNames := []string{ - "", // empty - strings.Repeat("1", MaxQualifiedNameLength+1), // too long - " ", // only spaces - "f8cc010c.ee72.4681;89d2+d46e1bd47d33", // invalid chars - } - - // group - for _, invalidGroup := range invalidQualifiedNames { - key = &resourcepb.ResourceKey{ - Group: invalidGroup, - Resource: "rrrr", // can be anything :( - Namespace: "default", - Name: "_IvIsOYGz", - } - - created, err = server.Create(ctx, &resourcepb.CreateRequest{ - Value: raw, - Key: key, - }) - - require.Error(t, err) - require.Nil(t, created) - } - - // resource - for _, invalidResource := range invalidQualifiedNames { - key = &resourcepb.ResourceKey{ - Group: "playlist.grafana.app", - Resource: invalidResource, - Namespace: "default", - Name: "_IvIsOYGz", - } - - created, err = server.Create(ctx, &resourcepb.CreateRequest{ - Value: raw, - Key: key, - }) - - require.Error(t, err) - require.Nil(t, created) - } - - // namespace - for _, invalidNamespace := range invalidQualifiedNames { - key = &resourcepb.ResourceKey{ - Group: "playlist.grafana.app", - Resource: "rrrr", // can be anything :( - Namespace: invalidNamespace, - Name: "_IvIsOYGz", - } - - created, err = server.Create(ctx, &resourcepb.CreateRequest{ - Value: raw, - Key: key, - }) - - require.Error(t, err) - require.Nil(t, created) - } - }) - t.Run("playlist update optimistic concurrency check", func(t *testing.T) { raw := []byte(`{ "apiVersion": "playlist.grafana.app/v0alpha1", diff --git a/pkg/storage/unified/resource/validation.go b/pkg/storage/unified/resource/validation.go index c3d3ee5bd45..2c1b5cf3740 100644 --- a/pkg/storage/unified/resource/validation.go +++ b/pkg/storage/unified/resource/validation.go @@ -1,15 +1,11 @@ package resource import ( - "fmt" "regexp" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" - "k8s.io/apimachinery/pkg/util/validation" ) -const MaxQualifiedNameLength = 40 - var validNameCharPattern = `a-zA-Z0-9:\-\_\.` var validNamePattern = regexp.MustCompile(`^[` + validNameCharPattern + `]*$`).MatchString @@ -28,19 +24,3 @@ func validateName(name string) *resourcepb.ErrorResult { // so we will be slightly more lenient than standard k8s return nil } - -func validateQualifiedName(value string) *resourcepb.ErrorResult { - if len(value) == 0 { - return NewBadRequestError("value is too short") - } - if len(value) > MaxQualifiedNameLength { - return NewBadRequestError("value is too long") - } - - err := validation.IsQualifiedName(value) - if len(err) > 0 { - return NewBadRequestError(fmt.Sprintf("name is not a valid qualified name: %+v", err)) - } - - return nil -} diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 95bb77c2dc0..572bc8eb224 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -88,6 +88,9 @@ type bleveBackend struct { cache map[resource.NamespacedResource]*bleveIndex indexMetrics *resource.BleveIndexMetrics + + metricsUpdaterCancel func() + metricsUpdaterWg sync.WaitGroup } func NewBleveBackend(opts BleveOptions, tracer trace.Tracer, indexMetrics *resource.BleveIndexMetrics) (*bleveBackend, error) { @@ -129,7 +132,14 @@ func NewBleveBackend(opts BleveOptions, tracer trace.Tracer, indexMetrics *resou indexMetrics: indexMetrics, } - go be.updateIndexSizeMetric(opts.Root) + if be.indexMetrics != nil { + ctx, cancel := context.WithCancel(context.Background()) + be.metricsUpdaterCancel = cancel + be.metricsUpdaterWg.Add(1) + go be.updateIndexSizeMetric(ctx, opts.Root) + } else { + be.metricsUpdaterCancel = func() { /* empty */ } + } return be, nil } @@ -184,18 +194,19 @@ func (b *bleveBackend) getCachedIndex(key resource.NamespacedResource) *bleveInd } // updateIndexSizeMetric sets the total size of all file-based indices metric. -func (b *bleveBackend) updateIndexSizeMetric(indexPath string) { - if b.indexMetrics == nil { - return - } +func (b *bleveBackend) updateIndexSizeMetric(ctx context.Context, indexPath string) { + defer b.metricsUpdaterWg.Done() - for { + for ctx.Err() == nil { var totalSize int64 err := filepath.WalkDir(indexPath, func(path string, info os.DirEntry, err error) error { if err != nil { return err } + if err = ctx.Err(); err != nil { + return err + } if !info.IsDir() { fileInfo, err := info.Info() if err != nil { @@ -212,7 +223,12 @@ func (b *bleveBackend) updateIndexSizeMetric(indexPath string) { b.log.Error("got error while trying to calculate bleve file index size", "error", err) } - time.Sleep(60 * time.Second) + select { + case <-ctx.Done(): + return + case <-time.After(60 * time.Second): + continue + } } } @@ -626,7 +642,15 @@ func (b *bleveBackend) findPreviousFileBasedIndex(resourceDir string, minBuildTi return nil, "", 0 } -func (b *bleveBackend) CloseAllIndexes() { +// Stop closes all indexes and stops background tasks. +func (b *bleveBackend) Stop() { + b.closeAllIndexes() + + b.metricsUpdaterCancel() + b.metricsUpdaterWg.Wait() +} + +func (b *bleveBackend) closeAllIndexes() { b.cacheMx.Lock() defer b.cacheMx.Unlock() diff --git a/pkg/storage/unified/search/bleve_integration_test.go b/pkg/storage/unified/search/bleve_integration_test.go index 36f19a0bfd3..9ee16a1b559 100644 --- a/pkg/storage/unified/search/bleve_integration_test.go +++ b/pkg/storage/unified/search/bleve_integration_test.go @@ -24,7 +24,7 @@ func TestBleveSearchBackend(t *testing.T) { require.NoError(t, err) require.NotNil(t, backend) - t.Cleanup(backend.CloseAllIndexes) + t.Cleanup(backend.Stop) return backend }, &unitest.TestOptions{ @@ -49,7 +49,7 @@ func TestSearchBackendBenchmark(t *testing.T) { require.NoError(t, err) require.NotNil(t, backend) - t.Cleanup(backend.CloseAllIndexes) + t.Cleanup(backend.Stop) unitest.BenchmarkSearchBackend(t, backend, opts) } diff --git a/pkg/storage/unified/search/bleve_search_test.go b/pkg/storage/unified/search/bleve_search_test.go index 3b65ba62316..f3ed214deb5 100644 --- a/pkg/storage/unified/search/bleve_search_test.go +++ b/pkg/storage/unified/search/bleve_search_test.go @@ -250,7 +250,7 @@ func newTestDashboardsIndex(t testing.TB, threshold int64, size int64, batchSize }, tracing.NewNoopTracerService(), nil) require.NoError(t, err) - t.Cleanup(backend.CloseAllIndexes) + t.Cleanup(backend.Stop) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{Namespace: "ns"}) diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index 324bba1f60f..18afe19abb2 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -41,8 +41,7 @@ func TestMain(m *testing.M) { goleak.VerifyTestMain(m, goleak.IgnoreTopFunction("github.com/open-feature/go-sdk/openfeature.(*eventExecutor).startEventListener.func1.1"), goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"), - goleak.IgnoreTopFunction("github.com/blevesearch/bleve_index_api.AnalysisWorker"), // These don't stop when index is closed. - goleak.IgnoreAnyFunction("github.com/grafana/grafana/pkg/storage/unified/search.(*bleveBackend).updateIndexSizeMetric"), // We don't have a way to stop this one yet. + goleak.IgnoreTopFunction("github.com/blevesearch/bleve_index_api.AnalysisWorker"), // These don't stop when index is closed. ) } @@ -66,7 +65,7 @@ func TestBleveBackend(t *testing.T) { }, tracing.NewNoopTracerService(), nil) require.NoError(t, err) - t.Cleanup(backend.CloseAllIndexes) + t.Cleanup(backend.Stop) rv := int64(10) ctx := identity.WithRequester(context.Background(), &user.SignedInUser{Namespace: "ns"}) @@ -783,7 +782,7 @@ func setupBleveBackend(t *testing.T, options ...setupOption) (*bleveBackend, pro backend, err := NewBleveBackend(opts, tracing.NewNoopTracerService(), metrics) require.NoError(t, err) require.NotNil(t, backend) - t.Cleanup(backend.CloseAllIndexes) + t.Cleanup(backend.Stop) return backend, reg } @@ -895,9 +894,9 @@ func TestCloseAllIndexes(t *testing.T) { // Verify two open indexes. checkOpenIndexes(t, reg, 1, 1) - backend1.CloseAllIndexes() + backend1.closeAllIndexes() - // Verify that there are no open indexes after CloseAllIndexes call. + // Verify that there are no open indexes after closeAllIndexes call. checkOpenIndexes(t, reg, 0, 0) } @@ -962,7 +961,7 @@ func TestBuildIndex(t *testing.T) { backend, _ := setupBleveBackend(t, withFileThreshold(5), withRootDir(tmpDir), withBuildVersion(version)) _, err := backend.BuildIndex(context.Background(), ns, firstIndexDocsCount, nil, "test", indexTestDocs(ns, firstIndexDocsCount, 100), nil, rebuild) require.NoError(t, err) - backend.CloseAllIndexes() + backend.Stop() } // Make sure we pass at least 1 nanosecond (alwaysRebuildDueToAge) to ensure that the index needs to be rebuild. diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index 0166a4a96f7..935f469c4af 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -110,7 +110,7 @@ func TestIntegrationSearchAndStorage(t *testing.T) { require.NoError(t, err) require.NotNil(t, search) - t.Cleanup(search.CloseAllIndexes) + t.Cleanup(search.Stop) // Create a new resource backend storage := newTestBackend(t, false, 0) diff --git a/pkg/tests/apis/alerting/notifications/common/testing.go b/pkg/tests/apis/alerting/notifications/common/testing.go index b004d65fc26..bd9891402c9 100644 --- a/pkg/tests/apis/alerting/notifications/common/testing.go +++ b/pkg/tests/apis/alerting/notifications/common/testing.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" ) func NewReceiverClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.Receiver, v0alpha1.ReceiverList] { diff --git a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go index 7af12d6f36f..1d065aab2fd 100644 --- a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go +++ b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go @@ -21,7 +21,8 @@ import ( "k8s.io/apimachinery/pkg/types" "github.com/grafana/alerting/notify" - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" @@ -1413,6 +1414,7 @@ func TestIntegrationReceiverListSelector(t *testing.T) { require.Len(t, receivers.Items, 3) // Includes default. t.Run("should filter by receiver name", func(t *testing.T) { + t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported list, err := adminClient.List(ctx, v1.ListOptions{ FieldSelector: "spec.title=" + recv1.Spec.Title, }) @@ -1431,6 +1433,7 @@ func TestIntegrationReceiverListSelector(t *testing.T) { }) t.Run("should filter by multiple filters", func(t *testing.T) { + t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported list, err := adminClient.List(ctx, v1.ListOptions{ FieldSelector: fmt.Sprintf("metadata.name=%s,spec.title=%s", recv2.Name, recv2.Spec.Title), }) diff --git a/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go b/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go index eb36da10ac7..00bae878f6d 100644 --- a/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go +++ b/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go @@ -16,10 +16,10 @@ import ( "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/routingtree" - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/fakes" + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/fakes" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" diff --git a/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go b/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go index d291d82faa3..04d9e626ab7 100644 --- a/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go +++ b/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go @@ -13,7 +13,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -600,6 +600,7 @@ func TestIntegrationListSelector(t *testing.T) { require.Len(t, tmpls.Items, 3) // Includes default template. t.Run("should filter by template name", func(t *testing.T) { + t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported list, err := adminClient.List(ctx, v1.ListOptions{ FieldSelector: "spec.title=" + template1.Spec.Title, }) @@ -618,6 +619,7 @@ func TestIntegrationListSelector(t *testing.T) { }) t.Run("should filter by multiple filters", func(t *testing.T) { + t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported list, err := adminClient.List(ctx, v1.ListOptions{ FieldSelector: fmt.Sprintf("metadata.name=%s,spec.title=%s", template2.Name, template2.Spec.Title), }) @@ -635,6 +637,7 @@ func TestIntegrationListSelector(t *testing.T) { }) t.Run("should filter by default template name", func(t *testing.T) { + t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported list, err := adminClient.List(ctx, v1.ListOptions{ FieldSelector: "spec.title=" + v0alpha1.DefaultTemplateTitle, }) diff --git a/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go b/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go index dfcb7ddb268..fdac61b048b 100644 --- a/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go +++ b/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go @@ -17,8 +17,8 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1" - "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1/fakes" + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1" + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1/fakes" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/routingtree" @@ -583,6 +583,7 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) { require.Len(t, intervals.Items, 2) t.Run("should filter by interval name", func(t *testing.T) { + t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported list, err := adminClient.List(ctx, v1.ListOptions{ FieldSelector: "spec.name=" + interval1.Spec.Name, }) @@ -601,6 +602,7 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) { }) t.Run("should filter by multiple filters", func(t *testing.T) { + t.Skip("disabled until app installer supports it") list, err := adminClient.List(ctx, v1.ListOptions{ FieldSelector: fmt.Sprintf("metadata.name=%s,spec.name=%s", interval2.Name, interval2.Spec.Name), }) diff --git a/pkg/tests/apis/folder/folder_tree_test.go b/pkg/tests/apis/folder/folder_tree_test.go index 8a22917327f..546a4d21aee 100644 --- a/pkg/tests/apis/folder/folder_tree_test.go +++ b/pkg/tests/apis/folder/folder_tree_test.go @@ -19,15 +19,13 @@ import ( "k8s.io/client-go/rest" dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" - "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/apimachinery/identity" + folderV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/utils" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/search/model" - "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/apis" "github.com/grafana/grafana/pkg/tests/testinfra" @@ -87,6 +85,10 @@ func TestIntegrationFolderTree(t *testing.T) { Children: []FolderDefinition{ {Name: "child", Creator: helper.Org1.Admin, + Permissions: []FolderPermission{{ + Permission: "View", + User: helper.Org1.None, + }}, }, }, }, @@ -95,18 +97,24 @@ func TestIntegrationFolderTree(t *testing.T) { }, }, Expected: []ExpectedTree{ - {Users: []apis.User{ - helper.Org1.Admin, - helper.Org1.Viewer, // By default, viewer can view all dashboards - }, Listing: ` - └── top - ....└── middle - ........└── child`}, - {Users: []apis.User{helper.Org1.None}, Listing: ``}, + {User: helper.Org1.Admin, Listing: ` + └── top (admin,edit,save,delete) + ....└── middle (admin,edit,save,delete) + ........└── child (admin,edit,save,delete)`}, + {User: helper.Org1.Viewer, Listing: ` + └── top (view) + ....└── middle (view) + ........└── child (view)`}, + {User: helper.Org1.None, Listing: ` + └── sharedwithme (???) + ....└── child (view)`, + E403: []string{"top", "middle"}, + }, }, }, } + var statusCode int for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { tt.Definition.RequireUniqueName(t, make(map[string]bool)) @@ -115,21 +123,56 @@ func TestIntegrationFolderTree(t *testing.T) { // CreateWithLegacyAPI for _, expect := range tt.Expected { - for _, user := range expect.Users { - t.Run(fmt.Sprintf("query as %s", user.Identity.GetLogin()), func(t *testing.T) { - legacy := getFoldersFromLegacyAPISearch(t, user) - legacy.requireEqual(t, expect.Listing, "legacy") + unstructured, client := getFolderClients(t, expect.User) + t.Run(fmt.Sprintf("query as %s", expect.User.Identity.GetLogin()), func(t *testing.T) { + legacy := getFoldersFromLegacyAPISearch(t, client) + legacy.requireEqual(t, expect.Listing, "legacy") - listed := getFoldersFromAPIServerList(t, user) - listed.requireEqual(t, expect.Listing, "listed") + listed := getFoldersFromAPIServerList(t, unstructured) + listed.requireEqual(t, expect.Listing, "listed") - search := getFoldersFromDashboardV0Search(t, user) - search.requireEqual(t, expect.Listing, "search") + search := getFoldersFromDashboardV0Search(t, client, expect.User.Identity.GetNamespace()) + search.requireEqual(t, expect.Listing, "search") - // ensure sure GET also works on each folder we can list - requireGettable(t, user, listed) + // ensure sure GET also works on each folder we can list + listed.forEach(func(fv *FolderView) { + if fv.Name == folder.SharedWithMeFolderUID { + return // skip it + } + found, err := unstructured.Get(context.Background(), fv.Name, v1.GetOptions{}) + require.NoErrorf(t, err, "getting folder: %s", fv.Name) + require.Equal(t, found.GetName(), fv.Name) }) - } + + // Forbidden things should really be hidden + for _, name := range expect.E403 { + _, err := unstructured.Get(context.Background(), name, v1.GetOptions{}) + require.Error(t, err) + require.Truef(t, apierrors.IsForbidden(err), "error: %w", err) // 404 vs 403 ???? + + result := client.Get().AbsPath("api", "folders", name). + Do(context.Background()). + StatusCode(&statusCode) + require.Equal(t, int(http.StatusForbidden), statusCode) + require.Error(t, result.Error()) + + // Verify sub-resources are hidden + for _, sub := range []string{"access", "parents", "children", "counts"} { + _, err := unstructured.Get(context.Background(), name, v1.GetOptions{}, sub) + require.Error(t, err, "expect error for subresource", sub) + require.Truef(t, apierrors.IsForbidden(err), "error: %w", err) // 404 vs 403 ???? + } + + // Verify legacy API access is also hidden + for _, sub := range []string{"permissions", "counts"} { + result := client.Get().AbsPath("api", "folders", name, sub). + Do(context.Background()). + StatusCode(&statusCode) + require.Equalf(t, int(http.StatusForbidden), statusCode, "legacy access to: %s", sub) + require.Error(t, result.Error()) + } + } + }) } }) } @@ -138,8 +181,9 @@ func TestIntegrationFolderTree(t *testing.T) { } type ExpectedTree struct { - Users []apis.User + User apis.User Listing string + E403 []string } type FolderDefinition struct { @@ -150,10 +194,10 @@ type FolderDefinition struct { } type FolderPermission struct { - User apis.User - Team team.Team - Role identity.RoleType - Access dashboardaccess.PermissionType + Permission string + User apis.User + // Team team.Team + // Role identity.RoleType } func (f *FolderDefinition) CreateWithLegacyAPI(t *testing.T, h *apis.K8sTestHelper, parent string) { @@ -183,39 +227,23 @@ func (f *FolderDefinition) CreateWithLegacyAPI(t *testing.T, h *apis.K8sTestHelp parent = f.Name if len(f.Permissions) > 0 { - cmd := dtos.UpdateDashboardACLCommand{} for _, def := range f.Permissions { - p := dtos.DashboardACLUpdateItem{ - TeamID: def.Team.ID, // likely zero - Role: &def.Role, - Permission: def.Access, - } + // http://localhost:3000/api/access-control/folders/feyx0ezuwqwowb/users/aeyx0jzgix9fkd + body = []byte(`{"permission": "` + def.Permission + `"}`) + require.NotEmpty(t, def.Permission, "invalid permission: %+v", def) if def.User.Identity != nil { - p.UserID, err = def.User.Identity.GetInternalID() - require.NoError(t, err) + result = client.Post().AbsPath( + "api", "access-control", "folders", f.Name, "users", def.User.Identity.GetIdentifier()). + Body(body). + SetHeader("Content-type", "application/json"). + Do(context.Background()). + StatusCode(&statusCode) + err = result.Error() + require.NoErrorf(t, err, "legacy access control: %s: %s", f.Name, body) + require.Equal(t, int(http.StatusOK), statusCode, f.Name) } - cmd.Items = append(cmd.Items, p) } - - body, err := json.Marshal(cmd) - require.NoError(t, err) - - var statusCode int // folders/{folder_uid}/permissions - result = client.Post().AbsPath("api", "folders", parent, "permissions"). - Body(body). - SetHeader("Content-type", "application/json"). - Do(context.Background()). - StatusCode(&statusCode) - require.NoError(t, result.Error(), f.Name) - require.Equal(t, int(http.StatusOK), statusCode, f.Name) } - - // Now check that we could get the folder - result = client.Get().AbsPath("api", "folders", f.Name). - Do(context.Background()). - StatusCode(&statusCode) - require.NoErrorf(t, result.Error(), "get folder after create: %s", f.Name) - require.Equal(t, int(http.StatusOK), statusCode, f.Name) } for _, child := range f.Children { @@ -272,6 +300,7 @@ type FolderView struct { Parent string Title string Children []*FolderView + Access *folderV1.FolderAccessInfo } func (n *FolderView) forEach(cb func(*FolderView)) { @@ -294,19 +323,37 @@ func (n *FolderView) requireEqual(t *testing.T, expect string, msg string) { require.Equal(t, expect, found, fmt.Sprintf("%s // EXPECT:\n%s\n\nFOUND:\n%s", msg, expect, found)) } +func accessDescription(access *folderV1.FolderAccessInfo) string { + if access == nil { + return "???" + } + perms := []string{} + if access.CanAdmin { + perms = append(perms, "admin") + } + if access.CanEdit { + perms = append(perms, "edit") + } + if access.CanSave { + perms = append(perms, "save") + } + if access.CanDelete { + perms = append(perms, "delete") + } + if len(perms) == 0 { + return "view" // because it was not nil! + } + return strings.Join(perms, ",") +} + func (n *FolderView) build(tree treeprint.Tree) treeprint.Tree { for _, child := range n.Children { - child.build(tree.AddBranch(child.Name)) + child.build(tree.AddBranch(fmt.Sprintf("%s (%s)", child.Name, accessDescription(child.Access)))) } return tree } -func getFoldersFromLegacyAPISearch(t *testing.T, who apis.User) *FolderView { - cfg := dynamic.ConfigFor(who.NewRestConfig()) - cfg.GroupVersion = &schema.GroupVersion{Group: "folder.grafana.app", Version: "v1beta1"} // group does not matter - client, err := rest.RESTClientFor(cfg) - require.NoError(t, err) - +func getFoldersFromLegacyAPISearch(t *testing.T, client *rest.RESTClient) *FolderView { var statusCode int result := client.Get().AbsPath("api", "search"). Param("type", "dash-folder"). @@ -324,41 +371,58 @@ func getFoldersFromLegacyAPISearch(t *testing.T, who apis.User) *FolderView { lookup := make(map[string]*FolderView, len(hits)) for _, hit := range hits { - lookup[hit.UID] = &FolderView{ + fv := &FolderView{ Name: hit.UID, Title: hit.Title, Parent: hit.FolderUID, } + + // Read the access info (note not the same model but the fields we care about do overlap) + result = client.Get().AbsPath("api", "folders", hit.UID). + Do(context.Background()). + StatusCode(&statusCode) + require.NoError(t, result.Error(), "getting folder access info (/api)") + require.Equal(t, int(http.StatusOK), statusCode) + + body, err := result.Raw() + require.NoError(t, err) + err = json.Unmarshal(body, &fv.Access) + require.NoError(t, err) + + lookup[hit.UID] = fv } - return makeRoot(t, lookup, "/api/search") + return makeRoot(lookup, "/api/search") } -func makeRoot(t *testing.T, lookup map[string]*FolderView, name string) *FolderView { +func makeRoot(lookup map[string]*FolderView, name string) *FolderView { + shared := &FolderView{} // when not found root := &FolderView{} for _, v := range lookup { if v.Parent == "" { root.Children = append(root.Children, v) } else { p, ok := lookup[v.Parent] - require.Truef(t, ok, "[%s] parent not found for: %s (parent:%s)", name, v.Name, v.Parent) - p.Children = append(p.Children, v) + if ok { + p.Children = append(p.Children, v) + } else { + shared.Children = append(shared.Children, v) + } } } + if len(shared.Children) > 0 { + shared.Name = folder.SharedWithMeFolderUID + root.Children = append([]*FolderView{shared}, root.Children...) + } return root } -func getFoldersFromDashboardV0Search(t *testing.T, who apis.User) *FolderView { - cfg := dynamic.ConfigFor(who.NewRestConfig()) - cfg.GroupVersion = &schema.GroupVersion{Group: "dashboard.grafana.app", Version: "v0alpha1"} // group does not matter - client, err := rest.RESTClientFor(cfg) - require.NoError(t, err) - +func getFoldersFromDashboardV0Search(t *testing.T, client *rest.RESTClient, ns string) *FolderView { var statusCode int - result := client.Get().AbsPath("apis", "dashboard.grafana.app", "v0alpha1", "namespaces", who.Identity.GetNamespace(), "search"). + result := client.Get().AbsPath("apis", "dashboard.grafana.app", "v0alpha1", "namespaces", ns, "search"). Param("limit", "1000"). Do(context.Background()). StatusCode(&statusCode) - err = result.Error() + err := result.Error() if err != nil { if apierrors.IsForbidden(err) { return &FolderView{} // empty list @@ -375,25 +439,45 @@ func getFoldersFromDashboardV0Search(t *testing.T, who apis.User) *FolderView { lookup := make(map[string]*FolderView, len(results.Hits)) for _, hit := range results.Hits { - lookup[hit.Name] = &FolderView{ + fv := &FolderView{ Name: hit.Name, Title: hit.Title, Parent: hit.Folder, } + + result = client.Get().AbsPath("apis", folderV1.APIGroup, + folderV1.APIVersion, "namespaces", ns, "folders", hit.Name, "access"). + Do(context.Background()). + StatusCode(&statusCode) + require.NoError(t, result.Error(), "getting folder access info (/access)") + require.Equal(t, int(http.StatusOK), statusCode) + + body, err := result.Raw() + require.NoError(t, err) + err = json.Unmarshal(body, &fv.Access) + require.NoError(t, err) + + lookup[hit.Name] = fv } - return makeRoot(t, lookup, "dashboards/search") + return makeRoot(lookup, "dashboards/search") } -func getFoldersFromAPIServerList(t *testing.T, who apis.User) *FolderView { - gvr := schema.GroupVersionResource{Group: "folder.grafana.app", Version: "v1beta1", Resource: "folders"} - +func getFolderClients(t *testing.T, who apis.User) (dynamic.ResourceInterface, *rest.RESTClient) { + gvr := schema.GroupVersionResource{Group: folderV1.APIGroup, Version: folderV1.APIVersion, Resource: "folders"} ns := who.Identity.GetNamespace() cfg := dynamic.ConfigFor(who.NewRestConfig()) dyn, err := dynamic.NewForConfig(cfg) require.NoError(t, err) - client := dyn.Resource(gvr).Namespace(ns) + dc := dyn.Resource(gvr).Namespace(ns) + cfg.GroupVersion = &schema.GroupVersion{Group: gvr.Group, Version: gvr.Version} + client, err := rest.RESTClientFor(cfg) + require.NoError(t, err) + return dc, client +} + +func getFoldersFromAPIServerList(t *testing.T, client dynamic.ResourceInterface) *FolderView { lookup := map[string]*FolderView{} continueToken := "" for { @@ -410,11 +494,20 @@ func getFoldersFromAPIServerList(t *testing.T, who apis.User) *FolderView { title, _, err := unstructured.NestedString(hit.Object, "spec", "title") require.NoError(t, err) - lookup[hit.GetName()] = &FolderView{ + fv := &FolderView{ Name: hit.GetName(), Title: title, Parent: obj.GetFolder(), } + + access, err := client.Get(context.Background(), fv.Name, v1.GetOptions{}, "access") + require.NoError(t, err) + jj, err := json.Marshal(access) + require.NoError(t, err) + err = json.Unmarshal(jj, &fv.Access) + require.NoError(t, err) + + lookup[fv.Name] = fv } continueToken = result.GetContinue() @@ -423,23 +516,7 @@ func getFoldersFromAPIServerList(t *testing.T, who apis.User) *FolderView { } } - return makeRoot(t, lookup, "folders/list") -} - -func requireGettable(t *testing.T, who apis.User, root *FolderView) { - gvr := schema.GroupVersionResource{Group: "folder.grafana.app", Version: "v1beta1", Resource: "folders"} - - ns := who.Identity.GetNamespace() - cfg := dynamic.ConfigFor(who.NewRestConfig()) - dyn, err := dynamic.NewForConfig(cfg) - require.NoError(t, err) - client := dyn.Resource(gvr).Namespace(ns) - - root.forEach(func(fv *FolderView) { - found, err := client.Get(context.Background(), fv.Name, v1.GetOptions{}) - require.NoErrorf(t, err, "getting folder: %s", fv.Name) - require.Equal(t, found.GetName(), fv.Name) - }) + return makeRoot(lookup, "folders/list") } func dotify(t treeprint.Tree) string { diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json index 514781017be..d3ac742e8fb 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json @@ -915,6 +915,836 @@ } ] }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/librarypanels": { + "get": { + "tags": [ + "LibraryPanel" + ], + "description": "list objects of kind LibraryPanel", + "operationId": "listLibraryPanel", + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "LibraryPanel" + } + }, + "post": { + "tags": [ + "LibraryPanel" + ], + "description": "create a LibraryPanel", + "operationId": "createLibraryPanel", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "LibraryPanel" + } + }, + "delete": { + "tags": [ + "LibraryPanel" + ], + "description": "delete collection of LibraryPanel", + "operationId": "deletecollectionLibraryPanel", + "parameters": [ + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "LibraryPanel" + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/librarypanels/{name}": { + "get": { + "tags": [ + "LibraryPanel" + ], + "description": "read the specified LibraryPanel", + "operationId": "getLibraryPanel", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "LibraryPanel" + } + }, + "put": { + "tags": [ + "LibraryPanel" + ], + "description": "replace the specified LibraryPanel", + "operationId": "replaceLibraryPanel", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "LibraryPanel" + } + }, + "delete": { + "tags": [ + "LibraryPanel" + ], + "description": "delete a LibraryPanel", + "operationId": "deleteLibraryPanel", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "LibraryPanel" + } + }, + "patch": { + "tags": [ + "LibraryPanel" + ], + "description": "partially update the specified LibraryPanel", + "operationId": "updateLibraryPanel", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "LibraryPanel" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the LibraryPanel", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search": { "get": { "tags": [ @@ -1089,6 +1919,139 @@ }, "components": { "schemas": { + "com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataQuery": { + "description": "Generic query properties", + "type": "object", + "properties": { + "datasource": { + "description": "The datasource", + "type": "object", + "required": [ + "type" + ], + "properties": { + "apiVersion": { + "description": "The apiserver version", + "type": "string" + }, + "type": { + "description": "The datasource plugin type", + "type": "string" + }, + "uid": { + "description": "Datasource UID (NOTE: name in k8s)", + "type": "string" + } + }, + "additionalProperties": false + }, + "hide": { + "description": "true if query is disabled (ie should not be returned to the dashboard)\nNOTE: this does not always imply that the query should not be executed since\nthe results from a hidden query may be used as the input to other queries (SSE etc)", + "type": "boolean" + }, + "intervalMs": { + "description": "Interval is the suggested duration between time points in a time series query.\nNOTE: the values for intervalMs is not saved in the query model. It is typically calculated\nfrom the interval required to fill a pixels in the visualization", + "type": "number" + }, + "maxDataPoints": { + "description": "MaxDataPoints is the maximum number of data points that should be returned from a time series query.\nNOTE: the values for maxDataPoints is not saved in the query model. It is typically calculated\nfrom the number of pixels visible in a visualization", + "type": "integer" + }, + "queryType": { + "description": "QueryType is an optional identifier for the type of query.\nIt can be used to distinguish different types of queries.", + "type": "string" + }, + "refId": { + "description": "RefID is the unique identifier of the query, set by the frontend call.", + "type": "string" + }, + "resultAssertions": { + "description": "Optionally define expected query result behavior", + "type": "object", + "required": [ + "typeVersion" + ], + "properties": { + "maxFrames": { + "description": "Maximum frame count", + "type": "integer" + }, + "type": { + "description": "Type asserts that the frame matches a known type structure.\n\n\nPossible enum values:\n - `\"\"` \n - `\"timeseries-wide\"` \n - `\"timeseries-long\"` \n - `\"timeseries-many\"` \n - `\"timeseries-multi\"` \n - `\"directory-listing\"` \n - `\"table\"` \n - `\"numeric-wide\"` \n - `\"numeric-multi\"` \n - `\"numeric-long\"` \n - `\"log-lines\"` ", + "type": "string", + "enum": [ + "", + "timeseries-wide", + "timeseries-long", + "timeseries-many", + "timeseries-multi", + "directory-listing", + "table", + "numeric-wide", + "numeric-multi", + "numeric-long", + "log-lines" + ], + "x-enum-description": {} + }, + "typeVersion": { + "description": "TypeVersion is the version of the Type property. Versions greater than 0.0 correspond to the dataplane\ncontract documentation https://grafana.github.io/dataplane/contract/.", + "type": "array", + "maxItems": 2, + "minItems": 2, + "items": { + "type": "integer" + } + } + }, + "additionalProperties": false + }, + "timeRange": { + "description": "TimeRange represents the query range\nNOTE: unlike generic /ds/query, we can now send explicit time values in each query\nNOTE: the values for timeRange are not saved in a dashboard, they are constructed on the fly", + "type": "object", + "required": [ + "from", + "to" + ], + "properties": { + "from": { + "description": "From is the start time of the query.", + "type": "string", + "default": "now-6h" + }, + "to": { + "description": "To is the end time of the query.", + "type": "string", + "default": "now" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true + }, + "com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataSourceRef": { + "description": "The datasource", + "type": "object", + "required": [ + "type" + ], + "properties": { + "apiVersion": { + "description": "The apiserver version", + "type": "string" + }, + "type": { + "description": "The datasource plugin type", + "type": "string" + }, + "uid": { + "description": "Datasource UID (NOTE: name in k8s)", + "type": "string" + } + }, + "additionalProperties": false + }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.AnnotationActions": { "type": "object", "required": [ @@ -1366,6 +2329,232 @@ } ] }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.GridPos": { + "type": "object", + "required": [ + "w", + "h", + "x", + "y" + ], + "properties": { + "h": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "w": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "y": { + "type": "integer", + "format": "int32", + "default": 0 + } + } + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel": { + "type": "object", + "required": [ + "spec" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "description": "Standard object's metadata More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "Panel properties", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanelSpec" + } + ] + }, + "status": { + "description": "Status will show errors", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanelStatus" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "LibraryPanel", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanelList": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "LibraryPanelList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanelSpec": { + "type": "object", + "required": [ + "type", + "options", + "fieldConfig" + ], + "properties": { + "datasource": { + "description": "The default datasource type", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataSourceRef" + } + ] + }, + "description": { + "description": "Library panel description", + "type": "string" + }, + "fieldConfig": { + "description": "The fieldConfig schema depends on the panel type", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + ] + }, + "gridPos": { + "description": "The grid position", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.GridPos" + } + ] + }, + "links": { + "description": "The links for the panel", + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + }, + "options": { + "description": "The options schema depends on the panel type", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + ] + }, + "panelTitle": { + "description": "The title of the panel when displayed in the dashboard", + "type": "string" + }, + "pluginVersion": { + "description": "The panel type", + "type": "string" + }, + "targets": { + "description": "The datasource queries", + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataQuery" + }, + "x-kubernetes-list-type": "set" + }, + "title": { + "description": "The title of the library panel", + "type": "string" + }, + "transparent": { + "description": "Whether the panel is transparent", + "type": "boolean" + }, + "type": { + "description": "The panel type", + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.LibraryPanelStatus": { + "type": "object", + "properties": { + "missing": { + "description": "The properties previously stored in SQL that are not included in this model", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + ] + }, + "warnings": { + "description": "Translation warnings (mostly things that were in SQL columns but not found in the saved body)", + "type": "array", + "items": { + "type": "string", + "default": "" + } + } + } + }, "com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured": { "type": "object", "additionalProperties": true, diff --git a/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json index 6240ab9fd57..4816ea0580f 100644 --- a/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json @@ -140,27 +140,27 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverList" } }, "application/json;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverList" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverList" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverList" } } } @@ -212,17 +212,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } } }, @@ -234,17 +234,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } } } @@ -254,17 +254,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } } } @@ -274,17 +274,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } } } @@ -486,17 +486,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } } } @@ -548,17 +548,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } } }, @@ -570,17 +570,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } } } @@ -590,17 +590,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } } } @@ -790,17 +790,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } } } @@ -810,17 +810,307 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "notifications.alerting.grafana.app", + "version": "v0alpha1", + "kind": "Receiver" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Receiver", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/{namespace}/receivers/{name}/status": { + "get": { + "tags": [ + "Receiver" + ], + "description": "read status of the specified Receiver", + "operationId": "getReceiverStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "notifications.alerting.grafana.app", + "version": "v0alpha1", + "kind": "Receiver" + } + }, + "put": { + "tags": [ + "Receiver" + ], + "description": "replace status of the specified Receiver", + "operationId": "replaceReceiverStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "notifications.alerting.grafana.app", + "version": "v0alpha1", + "kind": "Receiver" + } + }, + "patch": { + "tags": [ + "Receiver" + ], + "description": "partially update status of the specified Receiver", + "operationId": "updateReceiverStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } } } @@ -970,27 +1260,27 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeList" } }, "application/json;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeList" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeList" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeList" } } } @@ -1042,17 +1332,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } } }, @@ -1064,17 +1354,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } } } @@ -1084,17 +1374,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } } } @@ -1104,17 +1394,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } } } @@ -1316,17 +1606,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } } } @@ -1378,17 +1668,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } } }, @@ -1400,17 +1690,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } } } @@ -1420,17 +1710,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } } } @@ -1620,17 +1910,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } } } @@ -1640,17 +1930,307 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "notifications.alerting.grafana.app", + "version": "v0alpha1", + "kind": "RoutingTree" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the RoutingTree", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/{namespace}/routingtrees/{name}/status": { + "get": { + "tags": [ + "RoutingTree" + ], + "description": "read status of the specified RoutingTree", + "operationId": "getRoutingTreeStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "notifications.alerting.grafana.app", + "version": "v0alpha1", + "kind": "RoutingTree" + } + }, + "put": { + "tags": [ + "RoutingTree" + ], + "description": "replace status of the specified RoutingTree", + "operationId": "replaceRoutingTreeStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "notifications.alerting.grafana.app", + "version": "v0alpha1", + "kind": "RoutingTree" + } + }, + "patch": { + "tags": [ + "RoutingTree" + ], + "description": "partially update status of the specified RoutingTree", + "operationId": "updateRoutingTreeStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } } } @@ -1800,27 +2380,27 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroupList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupList" } }, "application/json;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroupList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupList" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroupList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroupList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupList" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroupList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupList" } } } @@ -1872,17 +2452,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } } }, @@ -1894,17 +2474,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } } } @@ -1914,17 +2494,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } } } @@ -1934,17 +2514,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } } } @@ -2146,17 +2726,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } } } @@ -2208,17 +2788,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } } }, @@ -2230,17 +2810,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } } } @@ -2250,17 +2830,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } } } @@ -2450,17 +3030,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } } } @@ -2470,17 +3050,307 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "notifications.alerting.grafana.app", + "version": "v0alpha1", + "kind": "TemplateGroup" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the TemplateGroup", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/{namespace}/templategroups/{name}/status": { + "get": { + "tags": [ + "TemplateGroup" + ], + "description": "read status of the specified TemplateGroup", + "operationId": "getTemplateGroupStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "notifications.alerting.grafana.app", + "version": "v0alpha1", + "kind": "TemplateGroup" + } + }, + "put": { + "tags": [ + "TemplateGroup" + ], + "description": "replace status of the specified TemplateGroup", + "operationId": "replaceTemplateGroupStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "notifications.alerting.grafana.app", + "version": "v0alpha1", + "kind": "TemplateGroup" + } + }, + "patch": { + "tags": [ + "TemplateGroup" + ], + "description": "partially update status of the specified TemplateGroup", + "operationId": "updateTemplateGroupStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } } } @@ -2630,27 +3500,27 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalList" } }, "application/json;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalList" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalList" } }, "application/vnd.kubernetes.protobuf;stream=watch": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalList" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalList" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalList" } } } @@ -2702,17 +3572,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } } }, @@ -2724,17 +3594,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } } } @@ -2744,17 +3614,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } } } @@ -2764,17 +3634,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } } } @@ -2976,17 +3846,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } } } @@ -3038,17 +3908,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } } }, @@ -3060,17 +3930,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } } } @@ -3080,17 +3950,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } } } @@ -3280,17 +4150,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } } } @@ -3300,17 +4170,307 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/vnd.kubernetes.protobuf": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } }, "application/yaml": { "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "notifications.alerting.grafana.app", + "version": "v0alpha1", + "kind": "TimeInterval" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the TimeInterval", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/{namespace}/timeintervals/{name}/status": { + "get": { + "tags": [ + "TimeInterval" + ], + "description": "read status of the specified TimeInterval", + "operationId": "getTimeIntervalStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "notifications.alerting.grafana.app", + "version": "v0alpha1", + "kind": "TimeInterval" + } + }, + "put": { + "tags": [ + "TimeInterval" + ], + "description": "replace status of the specified TimeInterval", + "operationId": "replaceTimeIntervalStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "notifications.alerting.grafana.app", + "version": "v0alpha1", + "kind": "TimeInterval" + } + }, + "patch": { + "tags": [ + "TimeInterval" + ], + "description": "partially update status of the specified TimeInterval", + "operationId": "updateTimeIntervalStatus", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } } } @@ -3358,13 +4518,8 @@ }, "components": { "schemas": { - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver": { "type": "object", - "required": [ - "metadata", - "spec", - "status" - ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -3383,21 +4538,10 @@ ] }, "spec": { - "description": "Spec is the spec of the Receiver", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverSpec" }, "status": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverStatus" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverStatus" } }, "x-kubernetes-group-version-kind": [ @@ -3408,7 +4552,7 @@ } ] }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverIntegration": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverIntegration": { "type": "object", "required": [ "type", @@ -3422,30 +4566,29 @@ "secureFields": { "type": "object", "additionalProperties": { - "type": "boolean", - "default": false + "type": "boolean" } }, "settings": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "additionalProperties": {} } }, "type": { - "type": "string", - "default": "" + "type": "string" }, "uid": { "type": "string" }, "version": { - "type": "string", - "default": "" + "type": "string" } - } + }, + "additionalProperties": false }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverList": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverList": { "type": "object", "required": [ "metadata", @@ -3462,7 +4605,7 @@ "default": {}, "allOf": [ { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.Receiver" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.Receiver" } ] } @@ -3488,55 +4631,7 @@ } ] }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverSpec": { - "type": "object", - "required": [ - "title", - "integrations" - ], - "properties": { - "integrations": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverIntegration" - } - ] - } - }, - "title": { - "type": "string", - "default": "" - } - } - }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverstatusOperatorState" - } - ] - } - } - } - }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.ReceiverstatusOperatorState": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverOperatorState": { "type": "object", "required": [ "lastEvaluation", @@ -3551,28 +4646,68 @@ "description": "details contains any extra information that is operator-specific", "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "additionalProperties": {} } }, "lastEvaluation": { "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string", - "default": "" + "type": "string" }, "state": { - "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "description": "state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.", "type": "string", - "default": "" + "enum": [ + "success", + "in_progress", + "failed" + ] } - } + }, + "additionalProperties": false }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverSpec": { "type": "object", "required": [ - "metadata", - "spec", - "status" + "title", + "integrations" ], + "properties": { + "integrations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverIntegration" + } + }, + "title": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.ReceiverOperatorState" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree": { + "type": "object", "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -3591,21 +4726,10 @@ ] }, "spec": { - "description": "Spec is the spec of the RoutingTree", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeSpec" }, "status": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeStatus" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeStatus" } }, "x-kubernetes-group-version-kind": [ @@ -3616,7 +4740,7 @@ } ] }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeList": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeList": { "type": "object", "required": [ "metadata", @@ -3633,7 +4757,7 @@ "default": {}, "allOf": [ { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTree" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTree" } ] } @@ -3659,7 +4783,7 @@ } ] }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeMatcher": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeMatcher": { "type": "object", "required": [ "type", @@ -3668,167 +4792,24 @@ ], "properties": { "label": { - "type": "string", - "default": "" + "type": "string" }, "type": { "type": "string", - "default": "" - }, - "value": { - "type": "string", - "default": "" - } - } - }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeRoute": { - "type": "object", - "required": [ - "continue" - ], - "properties": { - "active_time_intervals": { - "type": "array", - "items": { - "type": "string", - "default": "" - } - }, - "continue": { - "type": "boolean", - "default": false - }, - "group_by": { - "type": "array", - "items": { - "type": "string", - "default": "" - } - }, - "group_interval": { - "type": "string" - }, - "group_wait": { - "type": "string" - }, - "matchers": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeMatcher" - } - ] - } - }, - "mute_time_intervals": { - "type": "array", - "items": { - "type": "string", - "default": "" - } - }, - "receiver": { - "type": "string" - }, - "repeat_interval": { - "type": "string" - }, - "routes": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeRoute" - } - ] - } - } - } - }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeRouteDefaults": { - "type": "object", - "required": [ - "receiver" - ], - "properties": { - "group_by": { - "type": "array", - "items": { - "type": "string", - "default": "" - } - }, - "group_interval": { - "type": "string" - }, - "group_wait": { - "type": "string" - }, - "receiver": { - "type": "string", - "default": "" - }, - "repeat_interval": { - "type": "string" - } - } - }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeSpec": { - "type": "object", - "required": [ - "defaults", - "routes" - ], - "properties": { - "defaults": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeRouteDefaults" - } + "enum": [ + "=", + "!=", + "=~", + "!~" ] }, - "routes": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeRoute" - } - ] - } + "value": { + "type": "string" } - } + }, + "additionalProperties": false }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreeStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreestatusOperatorState" - } - ] - } - } - } - }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.RoutingTreestatusOperatorState": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeOperatorState": { "type": "object", "required": [ "lastEvaluation", @@ -3843,28 +4824,149 @@ "description": "details contains any extra information that is operator-specific", "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "additionalProperties": {} } }, "lastEvaluation": { "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string", - "default": "" + "type": "string" }, "state": { - "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "description": "state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.", "type": "string", - "default": "" + "enum": [ + "success", + "in_progress", + "failed" + ] } - } + }, + "additionalProperties": false }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeRoute": { "type": "object", "required": [ - "metadata", - "spec", - "status" + "continue" ], + "properties": { + "active_time_intervals": { + "type": "array", + "items": { + "type": "string" + } + }, + "continue": { + "type": "boolean" + }, + "group_by": { + "type": "array", + "items": { + "type": "string" + } + }, + "group_interval": { + "type": "string" + }, + "group_wait": { + "type": "string" + }, + "matchers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeMatcher" + } + }, + "mute_time_intervals": { + "type": "array", + "items": { + "type": "string" + } + }, + "receiver": { + "type": "string" + }, + "repeat_interval": { + "type": "string" + }, + "routes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeRoute" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeRouteDefaults": { + "type": "object", + "required": [ + "receiver" + ], + "properties": { + "group_by": { + "type": "array", + "items": { + "type": "string" + } + }, + "group_interval": { + "type": "string" + }, + "group_wait": { + "type": "string" + }, + "receiver": { + "type": "string" + }, + "repeat_interval": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeSpec": { + "type": "object", + "required": [ + "defaults", + "routes" + ], + "properties": { + "defaults": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeRouteDefaults" + }, + "routes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeRoute" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.RoutingTreeOperatorState" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup": { + "type": "object", "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -3883,21 +4985,10 @@ ] }, "spec": { - "description": "Spec is the spec of the TemplateGroup", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroupSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupSpec" }, "status": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroupStatus" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupStatus" } }, "x-kubernetes-group-version-kind": [ @@ -3908,7 +4999,7 @@ } ] }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroupList": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupList": { "type": "object", "required": [ "metadata", @@ -3925,7 +5016,7 @@ "default": {}, "allOf": [ { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroup" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroup" } ] } @@ -3951,48 +5042,7 @@ } ] }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroupSpec": { - "type": "object", - "required": [ - "title", - "content" - ], - "properties": { - "content": { - "type": "string", - "default": "" - }, - "title": { - "type": "string", - "default": "" - } - } - }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroupStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroupstatusOperatorState" - } - ] - } - } - } - }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TemplateGroupstatusOperatorState": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupOperatorState": { "type": "object", "required": [ "lastEvaluation", @@ -4007,28 +5057,65 @@ "description": "details contains any extra information that is operator-specific", "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "additionalProperties": {} } }, "lastEvaluation": { "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string", - "default": "" + "type": "string" }, "state": { - "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "description": "state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.", "type": "string", - "default": "" + "enum": [ + "success", + "in_progress", + "failed" + ] } - } + }, + "additionalProperties": false }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupSpec": { "type": "object", "required": [ - "metadata", - "spec", - "status" + "title", + "content" ], + "properties": { + "content": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TemplateGroupOperatorState" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval": { + "type": "object", "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", @@ -4047,21 +5134,10 @@ ] }, "spec": { - "description": "Spec is the spec of the TimeInterval", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalSpec" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalSpec" }, "status": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalStatus" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalStatus" } }, "x-kubernetes-group-version-kind": [ @@ -4072,14 +5148,13 @@ } ] }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalInterval": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalInterval": { "type": "object", "properties": { "days_of_month": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } }, "location": { @@ -4088,38 +5163,31 @@ "months": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } }, "times": { "type": "array", "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalTimeRange" - } - ] + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalTimeRange" } }, "weekdays": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } }, "years": { "type": "array", "items": { - "type": "string", - "default": "" + "type": "string" } } - } + }, + "additionalProperties": false }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalList": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalList": { "type": "object", "required": [ "metadata", @@ -4136,7 +5204,7 @@ "default": {}, "allOf": [ { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeInterval" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeInterval" } ] } @@ -4162,72 +5230,7 @@ } ] }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalSpec": { - "type": "object", - "required": [ - "name", - "time_intervals" - ], - "properties": { - "name": { - "type": "string", - "default": "" - }, - "time_intervals": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalInterval" - } - ] - } - } - } - }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalStatus": { - "type": "object", - "properties": { - "additionalFields": { - "description": "additionalFields is reserved for future use", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "operatorStates": { - "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", - "type": "object", - "additionalProperties": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalstatusOperatorState" - } - ] - } - } - } - }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalTimeRange": { - "type": "object", - "required": [ - "start_time", - "end_time" - ], - "properties": { - "end_time": { - "type": "string", - "default": "" - }, - "start_time": { - "type": "string", - "default": "" - } - } - }, - "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alerting.v0alpha1.TimeIntervalstatusOperatorState": { + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalOperatorState": { "type": "object", "required": [ "lastEvaluation", @@ -4242,20 +5245,81 @@ "description": "details contains any extra information that is operator-specific", "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "additionalProperties": {} } }, "lastEvaluation": { "description": "lastEvaluation is the ResourceVersion last evaluated", - "type": "string", - "default": "" + "type": "string" }, "state": { - "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "description": "state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.", "type": "string", - "default": "" + "enum": [ + "success", + "in_progress", + "failed" + ] } - } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalSpec": { + "type": "object", + "required": [ + "name", + "time_intervals" + ], + "properties": { + "name": { + "type": "string" + }, + "time_intervals": { + "type": "array", + "items": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalInterval" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalOperatorState" + } + } + }, + "additionalProperties": false + }, + "com.github.grafana.grafana.apps.alerting.notifications.pkg.apis.alertingnotifications.v0alpha1.TimeIntervalTimeRange": { + "type": "object", + "required": [ + "start_time", + "end_time" + ], + "properties": { + "end_time": { + "type": "string" + }, + "start_time": { + "type": "string" + } + }, + "additionalProperties": false }, "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { "description": "APIResource specifies the name of a resource and whether it is namespaced.", diff --git a/pkg/tests/apis/openapi_snapshots/preferences.grafana.app-v1alpha1.json b/pkg/tests/apis/openapi_snapshots/preferences.grafana.app-v1alpha1.json new file mode 100644 index 00000000000..e009c928585 --- /dev/null +++ b/pkg/tests/apis/openapi_snapshots/preferences.grafana.app-v1alpha1.json @@ -0,0 +1,2172 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Grafana preferences", + "title": "preferences.grafana.app/v1alpha1" + }, + "paths": { + "/apis/preferences.grafana.app/v1alpha1/": { + "get": { + "tags": [ + "API Discovery" + ], + "description": "Describe the available kubernetes resources", + "operationId": "getAPIResources", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + } + } + } + } + } + }, + "/apis/preferences.grafana.app/v1alpha1/namespaces/{namespace}/current": { + "get": { + "tags": [ + "Preferences" + ], + "description": "Get preferences for requester. This combines the user preferences with the team and global defaults", + "operationId": "currentPreferences", + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "workspace", + "required": true, + "schema": { + "type": "string" + }, + "example": "default" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/apis/preferences.grafana.app/v1alpha1/namespaces/{namespace}/preferences": { + "get": { + "tags": [ + "Preferences" + ], + "description": "list objects of kind Preferences", + "operationId": "listPreferences", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "preferences.grafana.app", + "version": "v1alpha1", + "kind": "Preferences" + } + }, + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + }, + "/apis/preferences.grafana.app/v1alpha1/namespaces/{namespace}/preferences/{name}": { + "get": { + "tags": [ + "Preferences" + ], + "description": "read the specified Preferences", + "operationId": "getPreferences", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Preferences" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Preferences" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Preferences" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "preferences.grafana.app", + "version": "v1alpha1", + "kind": "Preferences" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Preferences", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/preferences.grafana.app/v1alpha1/namespaces/{namespace}/stars": { + "get": { + "tags": [ + "Stars" + ], + "description": "list objects of kind Stars", + "operationId": "listStars", + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "preferences.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "post": { + "tags": [ + "Stars" + ], + "description": "create Stars", + "operationId": "createStars", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "preferences.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "delete": { + "tags": [ + "Stars" + ], + "description": "delete collection of Stars", + "operationId": "deletecollectionStars", + "parameters": [ + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "preferences.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/preferences.grafana.app/v1alpha1/namespaces/{namespace}/stars/{name}": { + "get": { + "tags": [ + "Stars" + ], + "description": "read the specified Stars", + "operationId": "getStars", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "preferences.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "put": { + "tags": [ + "Stars" + ], + "description": "replace the specified Stars", + "operationId": "replaceStars", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "preferences.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "delete": { + "tags": [ + "Stars" + ], + "description": "delete Stars", + "operationId": "deleteStars", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "preferences.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "patch": { + "tags": [ + "Stars" + ], + "description": "partially update the specified Stars", + "operationId": "updateStars", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "preferences.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Stars", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/preferences.grafana.app/v1alpha1/namespaces/{namespace}/stars/{name}/write/{group}/{kind}/{id}": { + "put": { + "tags": [ + "Stars" + ], + "description": "Add a starred item", + "operationId": "addStar", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "preferences.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "delete": { + "tags": [ + "Stars" + ], + "description": "Remove a starred item", + "operationId": "removeStar", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "preferences.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Stars", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "group", + "in": "path", + "description": "API group for stared item", + "required": true, + "schema": { + "type": "string" + }, + "example": "dashboard.grafana.app" + }, + { + "name": "kind", + "in": "path", + "description": "Kind for stared item", + "required": true, + "schema": { + "type": "string" + }, + "example": "Dashboard" + }, + { + "name": "id", + "in": "path", + "description": "The k8s name for the selected item", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "components": { + "schemas": { + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Preferences": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "Spec is the spec of the Preferences", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesSpec" + } + ] + }, + "status": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesStatus" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "preferences.grafana.app", + "kind": "Preferences", + "version": "v1alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesCookiePreferences": { + "type": "object", + "properties": { + "analytics": { + "type": "object" + }, + "functional": { + "type": "object" + }, + "performance": { + "type": "object" + } + } + }, + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Preferences" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "preferences.grafana.app", + "kind": "PreferencesList", + "version": "v1alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesNavbarPreference": { + "type": "object", + "required": [ + "bookmarkUrls" + ], + "properties": { + "bookmarkUrls": { + "type": "array", + "items": { + "type": "string", + "default": "" + } + } + } + }, + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesQueryHistoryPreference": { + "type": "object", + "properties": { + "homeTab": { + "description": "one of: '' | 'query' | 'starred';", + "type": "string" + } + } + }, + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesSpec": { + "type": "object", + "properties": { + "cookiePreferences": { + "description": "Cookie preferences", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesCookiePreferences" + } + ] + }, + "homeDashboardUID": { + "description": "UID for the home dashboard", + "type": "string" + }, + "language": { + "description": "Selected language (beta)", + "type": "string" + }, + "navbar": { + "description": "Navigation preferences", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesNavbarPreference" + } + ] + }, + "queryHistory": { + "description": "Explore query history preferences", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesQueryHistoryPreference" + } + ] + }, + "regionalFormat": { + "description": "Selected locale (beta)", + "type": "string" + }, + "theme": { + "description": "light, dark, empty is default", + "type": "string" + }, + "timezone": { + "description": "The timezone selection", + "type": "string" + }, + "weekStart": { + "description": "day of the week (sunday, monday, etc)", + "type": "string" + } + } + }, + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesstatusOperatorState" + } + ] + } + } + } + }, + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.PreferencesstatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars": { + "type": "object", + "required": [ + "metadata", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "Spec is the spec of the Stars", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsSpec" + } + ] + }, + "status": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsStatus" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "preferences.grafana.app", + "kind": "Stars", + "version": "v1alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "preferences.grafana.app", + "kind": "StarsList", + "version": "v1alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsResource": { + "type": "object", + "required": [ + "group", + "kind", + "names" + ], + "properties": { + "group": { + "type": "string", + "default": "" + }, + "kind": { + "type": "string", + "default": "" + }, + "names": { + "description": "The set of resources", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set" + } + } + }, + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsSpec": { + "type": "object", + "required": [ + "resource" + ], + "properties": { + "resource": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsResource" + } + ] + } + } + } + }, + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsStatus": { + "type": "object", + "properties": { + "additionalFields": { + "description": "additionalFields is reserved for future use", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "operatorStates": { + "description": "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + "type": "object", + "additionalProperties": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsstatusOperatorState" + } + ] + } + } + } + }, + "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsstatusOperatorState": { + "type": "object", + "required": [ + "lastEvaluation", + "state" + ], + "properties": { + "descriptiveState": { + "description": "descriptiveState is an optional more descriptive state field which has no requirements on format", + "type": "string" + }, + "details": { + "description": "details contains any extra information that is operator-specific", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "lastEvaluation": { + "description": "lastEvaluation is the ResourceVersion last evaluated", + "type": "string", + "default": "" + }, + "state": { + "description": "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + "type": "string", + "default": "" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { + "description": "APIResource specifies the name of a resource and whether it is namespaced.", + "type": "object", + "required": [ + "name", + "singularName", + "namespaced", + "kind", + "verbs" + ], + "properties": { + "categories": { + "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale\".", + "type": "string" + }, + "kind": { + "description": "kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo')", + "type": "string", + "default": "" + }, + "name": { + "description": "name is the plural name of the resource.", + "type": "string", + "default": "" + }, + "namespaced": { + "description": "namespaced indicates if a resource is namespaced or not.", + "type": "boolean", + "default": false + }, + "shortNames": { + "description": "shortNames is a list of suggested short names of the resource.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "singularName": { + "description": "singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface.", + "type": "string", + "default": "" + }, + "storageVersionHash": { + "description": "The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates.", + "type": "string" + }, + "verbs": { + "description": "verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy)", + "type": "array", + "items": { + "type": "string", + "default": "" + } + }, + "version": { + "description": "version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)\".", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { + "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", + "type": "object", + "required": [ + "groupVersion", + "resources" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "groupVersion": { + "description": "groupVersion is the group and version this APIResourceList is for.", + "type": "string", + "default": "" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "resources": { + "description": "resources contains the name of the resources and if they are namespaced.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResource" + } + ] + }, + "x-kubernetes-list-type": "atomic" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions": { + "description": "DeleteOptions may be provided when deleting an API object.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "dryRun": { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "gracePeriodSeconds": { + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "type": "integer", + "format": "int64" + }, + "ignoreStoreReadErrorWithClusterBreakingPotential": { + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "type": "boolean" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "orphanDependents": { + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "type": "boolean" + }, + "preconditions": { + "description": "Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions" + } + ] + }, + "propagationPolicy": { + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { + "description": "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", + "type": "object", + "properties": { + "continue": { + "description": "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", + "type": "string" + }, + "remainingItemCount": { + "description": "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", + "type": "integer", + "format": "int64" + }, + "resourceVersion": { + "description": "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": "string" + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": "string" + }, + "fieldsV1": { + "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1" + } + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": "string" + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": "string" + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": "string" + }, + "time": { + "description": "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "type": "object", + "properties": { + "annotations": { + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "creationTimestamp": { + "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "type": "integer", + "format": "int64" + }, + "deletionTimestamp": { + "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set", + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": "string" + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "type": "integer", + "format": "int64" + }, + "labels": { + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": "string" + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference" + } + ] + }, + "x-kubernetes-list-map-keys": [ + "uid" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "type": "object", + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string", + "default": "" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": "boolean" + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": "boolean" + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string", + "default": "" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string", + "default": "" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string", + "default": "" + } + }, + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Patch": { + "description": "Patch is provided to give a concrete name and type to the Kubernetes PATCH request body.", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions": { + "description": "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.", + "type": "object", + "properties": { + "resourceVersion": { + "description": "Specifies the target ResourceVersion", + "type": "string" + }, + "uid": { + "description": "Specifies the target UID.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Status": { + "description": "Status is a return value for calls that don't return other objects.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "code": { + "description": "Suggested HTTP return code for this status, 0 if not set.", + "type": "integer", + "format": "int32" + }, + "details": { + "description": "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails" + } + ], + "x-kubernetes-list-type": "atomic" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "message": { + "description": "A human-readable description of the status of this operation.", + "type": "string" + }, + "metadata": { + "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + }, + "reason": { + "description": "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", + "type": "string" + }, + "status": { + "description": "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause": { + "description": "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", + "type": "object", + "properties": { + "field": { + "description": "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", + "type": "string" + }, + "message": { + "description": "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", + "type": "string" + }, + "reason": { + "description": "A machine-readable description of the cause of the error. If this value is empty there is no information available.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails": { + "description": "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", + "type": "object", + "properties": { + "causes": { + "description": "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "The group attribute of the resource associated with the status StatusReason.", + "type": "string" + }, + "kind": { + "description": "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", + "type": "string" + }, + "retryAfterSeconds": { + "description": "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", + "type": "integer", + "format": "int32" + }, + "uid": { + "description": "UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "type": "string", + "format": "date-time" + } + } + } +} \ No newline at end of file diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index e3e1e99cf46..a221df08303 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -24,7 +24,7 @@ func TestIntegrationOpenAPIs(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) h := NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, + AppModeProduction: false, // required for experimental APIs EnableFeatureToggles: []string{ featuremgmt.FlagQueryService, // Query Library featuremgmt.FlagProvisioning, @@ -93,6 +93,9 @@ func TestIntegrationOpenAPIs(t *testing.T) { }, { Group: "playlist.grafana.app", Version: "v0alpha1", + }, { + Group: "preferences.grafana.app", + Version: "v1alpha1", }, { Group: "notifications.alerting.grafana.app", Version: "v0alpha1", diff --git a/public/app/api/clients/preferences/v1alpha1/baseAPI.ts b/public/app/api/clients/preferences/v1alpha1/baseAPI.ts new file mode 100644 index 00000000000..326e3e772dd --- /dev/null +++ b/public/app/api/clients/preferences/v1alpha1/baseAPI.ts @@ -0,0 +1,14 @@ +import { createApi } from '@reduxjs/toolkit/query/react'; + +import { createBaseQuery } from 'app/api/createBaseQuery'; +import { getAPIBaseURL } from 'app/api/utils'; + +export const BASE_URL = getAPIBaseURL('preferences.grafana.app', 'v1alpha1'); + +export const api = createApi({ + reducerPath: 'preferencesAPIv1alpha1', + baseQuery: createBaseQuery({ + baseURL: BASE_URL, + }), + endpoints: () => ({}), +}); diff --git a/public/app/api/clients/preferences/v1alpha1/endpoints.gen.ts b/public/app/api/clients/preferences/v1alpha1/endpoints.gen.ts new file mode 100644 index 00000000000..09d17608083 --- /dev/null +++ b/public/app/api/clients/preferences/v1alpha1/endpoints.gen.ts @@ -0,0 +1,719 @@ +import { api } from './baseAPI'; +export const addTagTypes = ['API Discovery', 'Preferences', 'Stars'] as const; +const injectedRtkApi = api + .enhanceEndpoints({ + addTagTypes, + }) + .injectEndpoints({ + endpoints: (build) => ({ + getApiResources: build.query({ + query: () => ({ url: `/apis/preferences.grafana.app/v1alpha1/` }), + providesTags: ['API Discovery'], + }), + currentPreferences: build.query({ + query: () => ({ url: `/current` }), + providesTags: ['Preferences'], + }), + listPreferences: build.query({ + query: (queryArg) => ({ + url: `/preferences`, + params: { + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + pretty: queryArg.pretty, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['Preferences'], + }), + getPreferences: build.query({ + query: (queryArg) => ({ + url: `/preferences/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['Preferences'], + }), + listStars: build.query({ + query: (queryArg) => ({ + url: `/stars`, + params: { + pretty: queryArg.pretty, + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['Stars'], + }), + createStars: build.mutation({ + query: (queryArg) => ({ + url: `/stars`, + method: 'POST', + body: queryArg.stars, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Stars'], + }), + deletecollectionStars: build.mutation({ + query: (queryArg) => ({ + url: `/stars`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['Stars'], + }), + getStars: build.query({ + query: (queryArg) => ({ + url: `/stars/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['Stars'], + }), + replaceStars: build.mutation({ + query: (queryArg) => ({ + url: `/stars/${queryArg.name}`, + method: 'PUT', + body: queryArg.stars, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Stars'], + }), + deleteStars: build.mutation({ + query: (queryArg) => ({ + url: `/stars/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['Stars'], + }), + updateStars: build.mutation({ + query: (queryArg) => ({ + url: `/stars/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['Stars'], + }), + addStar: build.mutation({ + query: (queryArg) => ({ + url: `/stars/${queryArg.name}/write/${queryArg.group}/${queryArg.kind}/${queryArg.id}`, + method: 'PUT', + }), + invalidatesTags: ['Stars'], + }), + removeStar: build.mutation({ + query: (queryArg) => ({ + url: `/stars/${queryArg.name}/write/${queryArg.group}/${queryArg.kind}/${queryArg.id}`, + method: 'DELETE', + }), + invalidatesTags: ['Stars'], + }), + }), + overrideExisting: false, + }); +export { injectedRtkApi as generatedAPI }; +export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList; +export type GetApiResourcesApiArg = void; +export type CurrentPreferencesApiResponse = /** status 200 undefined */ any; +export type CurrentPreferencesApiArg = void; +export type ListPreferencesApiResponse = /** status 200 OK */ PreferencesList; +export type ListPreferencesApiArg = { + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type GetPreferencesApiResponse = /** status 200 OK */ Preferences; +export type GetPreferencesApiArg = { + /** name of the Preferences */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ListStarsApiResponse = /** status 200 OK */ StarsList; +export type ListStarsApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type CreateStarsApiResponse = /** status 200 OK */ + | Stars + | /** status 201 Created */ Stars + | /** status 202 Accepted */ Stars; +export type CreateStarsApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + stars: Stars; +}; +export type DeletecollectionStarsApiResponse = /** status 200 OK */ Status; +export type DeletecollectionStarsApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; +export type GetStarsApiResponse = /** status 200 OK */ Stars; +export type GetStarsApiArg = { + /** name of the Stars */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceStarsApiResponse = /** status 200 OK */ Stars | /** status 201 Created */ Stars; +export type ReplaceStarsApiArg = { + /** name of the Stars */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + stars: Stars; +}; +export type DeleteStarsApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteStarsApiArg = { + /** name of the Stars */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; +export type UpdateStarsApiResponse = /** status 200 OK */ Stars | /** status 201 Created */ Stars; +export type UpdateStarsApiArg = { + /** name of the Stars */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; +export type AddStarApiResponse = /** status 200 OK */ Stars; +export type AddStarApiArg = { + /** name of the Stars */ + name: string; + /** API group for stared item */ + group: string; + /** Kind for stared item */ + kind: string; + /** The k8s name for the selected item */ + id: string; +}; +export type RemoveStarApiResponse = /** status 200 OK */ Stars; +export type RemoveStarApiArg = { + /** name of the Stars */ + name: string; + /** API group for stared item */ + group: string; + /** Kind for stared item */ + kind: string; + /** The k8s name for the selected item */ + id: string; +}; +export type ApiResource = { + /** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */ + categories?: string[]; + /** group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale". */ + group?: string; + /** kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo') */ + kind: string; + /** name is the plural name of the resource. */ + name: string; + /** namespaced indicates if a resource is namespaced or not. */ + namespaced: boolean; + /** shortNames is a list of suggested short names of the resource. */ + shortNames?: string[]; + /** singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface. */ + singularName: string; + /** The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates. */ + storageVersionHash?: string; + /** verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy) */ + verbs: string[]; + /** version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)". */ + version?: string; +}; +export type ApiResourceList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** groupVersion is the group and version this APIResourceList is for. */ + groupVersion: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** resources contains the name of the resources and if they are namespaced. */ + resources: ApiResource[]; +}; +export type Time = string; +export type FieldsV1 = object; +export type ManagedFieldsEntry = { + /** APIVersion defines the version of this resource that this field set applies to. The format is "group/version" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted. */ + apiVersion?: string; + /** FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: "FieldsV1" */ + fieldsType?: string; + /** FieldsV1 holds the first JSON version format as described in the "FieldsV1" type. */ + fieldsV1?: FieldsV1; + /** Manager is an identifier of the workflow managing these fields. */ + manager?: string; + /** Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'. */ + operation?: string; + /** Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource. */ + subresource?: string; + /** Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over. */ + time?: Time; +}; +export type OwnerReference = { + /** API version of the referent. */ + apiVersion: string; + /** If true, AND if the owner has the "foregroundDeletion" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs "delete" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned. */ + blockOwnerDeletion?: boolean; + /** If true, this reference points to the managing controller. */ + controller?: boolean; + /** Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind: string; + /** Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names */ + name: string; + /** UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ + uid: string; +}; +export type ObjectMeta = { + /** Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations */ + annotations?: { + [key: string]: string; + }; + /** CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. + + Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ + creationTimestamp?: Time; + /** Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only. */ + deletionGracePeriodSeconds?: number; + /** DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested. + + Populated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ + deletionTimestamp?: Time; + /** Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list. */ + finalizers?: string[]; + /** GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server. + + If this field is specified and the generated name exists, the server will return a 409. + + Applied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency */ + generateName?: string; + /** A sequence number representing a specific generation of the desired state. Populated by the system. Read-only. */ + generation?: number; + /** Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels */ + labels?: { + [key: string]: string; + }; + /** ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like "ci-cd". The set of fields is always in the version that the workflow used when modifying the object. */ + managedFields?: ManagedFieldsEntry[]; + /** Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names */ + name?: string; + /** Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty. + + Must be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces */ + namespace?: string; + /** List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller. */ + ownerReferences?: OwnerReference[]; + /** An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources. + + Populated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */ + resourceVersion?: string; + /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ + selfLink?: string; + /** UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations. + + Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ + uid?: string; +}; +export type PreferencesCookiePreferences = { + analytics?: object; + functional?: object; + performance?: object; +}; +export type PreferencesNavbarPreference = { + bookmarkUrls: string[]; +}; +export type PreferencesQueryHistoryPreference = { + /** one of: '' | 'query' | 'starred'; */ + homeTab?: string; +}; +export type PreferencesSpec = { + /** Cookie preferences */ + cookiePreferences?: PreferencesCookiePreferences; + /** UID for the home dashboard */ + homeDashboardUID?: string; + /** Selected language (beta) */ + language?: string; + /** Navigation preferences */ + navbar?: PreferencesNavbarPreference; + /** Explore query history preferences */ + queryHistory?: PreferencesQueryHistoryPreference; + /** Selected locale (beta) */ + regionalFormat?: string; + /** light, dark, empty is default */ + theme?: string; + /** The timezone selection */ + timezone?: string; + /** day of the week (sunday, monday, etc) */ + weekStart?: string; +}; +export type PreferencesstatusOperatorState = { + /** descriptiveState is an optional more descriptive state field which has no requirements on format */ + descriptiveState?: string; + /** details contains any extra information that is operator-specific */ + details?: { + [key: string]: object; + }; + /** lastEvaluation is the ResourceVersion last evaluated */ + lastEvaluation: string; + /** state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation. */ + state: string; +}; +export type PreferencesStatus = { + /** additionalFields is reserved for future use */ + additionalFields?: { + [key: string]: object; + }; + /** operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field. */ + operatorStates?: { + [key: string]: PreferencesstatusOperatorState; + }; +}; +export type Preferences = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ObjectMeta; + /** Spec is the spec of the Preferences */ + spec: PreferencesSpec; + status: PreferencesStatus; +}; +export type ListMeta = { + /** continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message. */ + continue?: string; + /** remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact. */ + remainingItemCount?: number; + /** String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */ + resourceVersion?: string; + /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ + selfLink?: string; +}; +export type PreferencesList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: Preferences[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ListMeta; +}; +export type StarsResource = { + group: string; + kind: string; + /** The set of resources */ + names: string[]; +}; +export type StarsSpec = { + resource: StarsResource[]; +}; +export type StarsstatusOperatorState = { + /** descriptiveState is an optional more descriptive state field which has no requirements on format */ + descriptiveState?: string; + /** details contains any extra information that is operator-specific */ + details?: { + [key: string]: object; + }; + /** lastEvaluation is the ResourceVersion last evaluated */ + lastEvaluation: string; + /** state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation. */ + state: string; +}; +export type StarsStatus = { + /** additionalFields is reserved for future use */ + additionalFields?: { + [key: string]: object; + }; + /** operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field. */ + operatorStates?: { + [key: string]: StarsstatusOperatorState; + }; +}; +export type Stars = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ObjectMeta; + /** Spec is the spec of the Stars */ + spec: StarsSpec; + status: StarsStatus; +}; +export type StarsList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: Stars[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ListMeta; +}; +export type StatusCause = { + /** The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional. + + Examples: + "name" - the field "name" on the current resource + "items[0].name" - the field "name" on the first array entry in "items" */ + field?: string; + /** A human-readable description of the cause of the error. This field may be presented as-is to a reader. */ + message?: string; + /** A machine-readable description of the cause of the error. If this value is empty there is no information available. */ + reason?: string; +}; +export type StatusDetails = { + /** The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes. */ + causes?: StatusCause[]; + /** The group attribute of the resource associated with the status StatusReason. */ + group?: string; + /** The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described). */ + name?: string; + /** If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action. */ + retryAfterSeconds?: number; + /** UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ + uid?: string; +}; +export type Status = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Suggested HTTP return code for this status, 0 if not set. */ + code?: number; + /** Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type. */ + details?: StatusDetails; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** A human-readable description of the status of this operation. */ + message?: string; + /** Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + metadata?: ListMeta; + /** A machine-readable description of why this operation is in the "Failure" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it. */ + reason?: string; + /** Status of the operation. One of: "Success" or "Failure". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status */ + status?: string; +}; +export type Patch = object; +export const { + useGetApiResourcesQuery, + useCurrentPreferencesQuery, + useListPreferencesQuery, + useGetPreferencesQuery, + useListStarsQuery, + useCreateStarsMutation, + useDeletecollectionStarsMutation, + useGetStarsQuery, + useReplaceStarsMutation, + useDeleteStarsMutation, + useUpdateStarsMutation, + useAddStarMutation, + useRemoveStarMutation, +} = injectedRtkApi; diff --git a/public/app/api/clients/preferences/v1alpha1/index.ts b/public/app/api/clients/preferences/v1alpha1/index.ts new file mode 100644 index 00000000000..da06eabd3fb --- /dev/null +++ b/public/app/api/clients/preferences/v1alpha1/index.ts @@ -0,0 +1,43 @@ +import { t } from '@grafana/i18n'; +import { notifyApp } from 'app/core/actions'; +import { createSuccessNotification, createErrorNotification } from 'app/core/copy/appNotification'; + +import { generatedAPI } from './endpoints.gen'; + +export const preferencesAPIv1alpha1 = generatedAPI.enhanceEndpoints({ + endpoints: { + addStar: { + onQueryStarted: async (_, { queryFulfilled, dispatch }) => { + try { + await queryFulfilled; + dispatch(notifyApp(createSuccessNotification(t('dashboard.toolbar.star-added', 'Added to starred')))); + } catch (e) { + if (e instanceof Error) { + dispatch( + notifyApp(createErrorNotification(t('dashboard.toolbar.star-add-error', 'Failed to add to starred'), e)) + ); + } + } + }, + }, + removeStar: { + onQueryStarted: async (_, { queryFulfilled, dispatch }) => { + try { + await queryFulfilled; + dispatch(notifyApp(createSuccessNotification(t('dashboard.toolbar.star-removed', 'Removed from starred')))); + } catch (e) { + if (e instanceof Error) { + dispatch( + notifyApp( + createErrorNotification(t('dashboard.toolbar.star-remove-error', 'Failed to remove from starred'), e) + ) + ); + } + } + }, + }, + }, +}); + +// eslint-disable-next-line no-barrel-files/no-barrel-files +export * from './endpoints.gen'; diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index 056c4bb0015..1e0d2791f31 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -3,6 +3,7 @@ import { AnyAction, combineReducers } from 'redux'; import { alertingAPI as alertingPackageAPI } from '@grafana/alerting/unstable'; import { dashboardAPIv0alpha1 } from 'app/api/clients/dashboard/v0alpha1'; +import { preferencesAPIv1alpha1 } from 'app/api/clients/preferences/v1alpha1'; import { rulesAPIv0alpha1 } from 'app/api/clients/rules/v0alpha1'; import { shortURLAPIv1alpha1 } from 'app/api/clients/shorturl/v1alpha1'; import sharedReducers from 'app/core/reducers'; @@ -77,6 +78,7 @@ const rootReducers = { [dashboardAPIv0alpha1.reducerPath]: dashboardAPIv0alpha1.reducer, [rulesAPIv0alpha1.reducerPath]: rulesAPIv0alpha1.reducer, [shortURLAPIv1alpha1.reducerPath]: shortURLAPIv1alpha1.reducer, + [preferencesAPIv1alpha1.reducerPath]: preferencesAPIv1alpha1.reducer, // PLOP_INJECT_REDUCER // Used by the API client generator }; diff --git a/public/app/features/alerting/unified/api/annotations.test.ts b/public/app/features/alerting/unified/api/annotations.test.ts index f80d7e8f22c..4854ec87d19 100644 --- a/public/app/features/alerting/unified/api/annotations.test.ts +++ b/public/app/features/alerting/unified/api/annotations.test.ts @@ -10,6 +10,10 @@ const get = jest.fn(() => { }); jest.mock('@grafana/runtime', () => ({ + config: { + namespace: 'default', + featureToggles: {}, + }, getBackendSrv: () => ({ get, }), diff --git a/public/app/features/alerting/unified/api/annotations.ts b/public/app/features/alerting/unified/api/annotations.ts index 2e0d577ffbf..56002e6b40d 100644 --- a/public/app/features/alerting/unified/api/annotations.ts +++ b/public/app/features/alerting/unified/api/annotations.ts @@ -1,11 +1,9 @@ -import { getBackendSrv } from '@grafana/runtime'; +import { annotationServer } from 'app/features/annotations/api'; import { StateHistoryItem } from 'app/types/unified-alerting'; export function fetchAnnotations(alertUID: string): Promise { - return getBackendSrv() - .get('/api/annotations', { - alertUID, - }) + return annotationServer() + .forAlert(alertUID) .then((result) => { return result?.sort(sortStateHistory); }); diff --git a/public/app/features/alerting/unified/components/AlertStateDot.tsx b/public/app/features/alerting/unified/components/AlertStateDot.tsx deleted file mode 100644 index ad6f8308f5e..00000000000 --- a/public/app/features/alerting/unified/components/AlertStateDot.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { css } from '@emotion/css'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Stack, useStyles2 } from '@grafana/ui'; - -interface DotStylesProps { - color: 'success' | 'error' | 'warning' | 'info'; - includeState?: boolean; -} - -const AlertStateDot = ({ color, includeState }: DotStylesProps) => { - const styles = useStyles2(getDotStyles, { color, includeState }); - - return ( - -
- - ); -}; - -const getDotStyles = (theme: GrafanaTheme2, props: DotStylesProps) => { - const size = theme.spacing(1.25); - const outlineSize = `calc(${size} / 2.5)`; - - const errorStyle = props.color === 'error'; - const successStyle = props.color === 'success'; - const warningStyle = props.color === 'warning'; - - return { - dot: css( - { - width: size, - height: size, - - borderRadius: theme.shape.radius.circle, - - backgroundColor: theme.colors.secondary.main, - outline: `solid ${outlineSize} ${theme.colors.secondary.transparent}`, - margin: outlineSize, - }, - successStyle && - css({ - backgroundColor: theme.colors.success.main, - outlineColor: theme.colors.success.transparent, - }), - warningStyle && - css({ - backgroundColor: theme.colors.warning.main, - outlineColor: theme.colors.warning.transparent, - }), - errorStyle && - css({ - backgroundColor: theme.colors.error.main, - outlineColor: theme.colors.error.transparent, - }) - ), - }; -}; - -export { AlertStateDot }; diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx index 2af4f973bed..76dbd89b9e2 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx @@ -303,7 +303,19 @@ function OptionCustomfield({