diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 46bb7ec1a6b..792354e405d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -599,9 +599,6 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/AppWrapper.tsx @grafana/frontend-ops /public/app/partials/ @grafana/grafana-frontend-platform - - - /scripts/benchmark-access-control.sh @grafana/access-squad /scripts/check-breaking-changes.sh @grafana/plugins-platform-frontend /scripts/ci-* @grafana/grafana-developer-enablement-squad @@ -740,6 +737,10 @@ embed.go @grafana/grafana-as-code /public/app/plugins/*gen.go @grafana/grafana-as-code /cue.mod/ @grafana/grafana-as-code +# Rendered OpenAPI from app platform +# Eventually each file owned by the right team, OR a structure with the rendered value under /apis/{group}/openapi +/openapi/ @grafana/grafana-app-platform-squad + # GitHub Workflows and Templates /.github/CODEOWNERS @tolzhabayev /.github/ISSUE_TEMPLATE/ @torkelo @sympatheticmoose diff --git a/Makefile b/Makefile index 3df11c69545..2721c2830b8 100644 --- a/Makefile +++ b/Makefile @@ -421,6 +421,7 @@ protobuf: ## Compile protobuf definitions buf generate pkg/plugins/backendplugin/secretsmanagerplugin --template pkg/plugins/backendplugin/secretsmanagerplugin/buf.gen.yaml buf generate pkg/storage/unified/resource --template pkg/storage/unified/resource/buf.gen.yaml buf generate pkg/services/authz/proto/v1 --template pkg/services/authz/proto/v1/buf.gen.yaml + buf generate pkg/services/ngalert/store/proto/v1 --template pkg/services/ngalert/store/proto/v1/buf.gen.yaml .PHONY: clean clean: ## Clean up intermediate build artifacts. diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 68bb56787e1..3038e1624b3 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -191,6 +191,7 @@ Experimental features might be changed or removed without prior notice. | `tableSharedCrosshair` | Enables shared crosshair in table panel | | `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | | `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | +| `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage | | `scopeApi` | In-development feature flag for the scope api using the app platform. | | `sqlExpressions` | Enables using SQL and DuckDB functions as Expressions. | | `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | diff --git a/go.work.sum b/go.work.sum index 44df698dd12..e9e332a9bac 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1503,6 +1503,7 @@ github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grafana/alerting v0.0.0-20250115195200-209e052dba64/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:V63rh3udd7sqXJeaG+nGUmViwVnM/bY6t8U9Tols2GU= github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 h1:3DHH81RJCi8Bcgn2MdBh7vgWUshmAFjZzBCVuxiQ0uk= github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= diff --git a/openapi/README.md b/openapi/README.md new file mode 100644 index 00000000000..93fdfdbeba9 --- /dev/null +++ b/openapi/README.md @@ -0,0 +1,3 @@ +This folder contains a rendered OpenAPI for each group/version + +The "real" openapi is generated by the running server, but this is used to build static frontend clients diff --git a/openapi/dashboard.grafana.app-v0alpha1.json b/openapi/dashboard.grafana.app-v0alpha1.json new file mode 100644 index 00000000000..2cf10939f94 --- /dev/null +++ b/openapi/dashboard.grafana.app-v0alpha1.json @@ -0,0 +1,2767 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Grafana dashboards as resources", + "title": "dashboard.grafana.app/v0alpha1" + }, + "paths": { + "/apis/dashboard.grafana.app/v0alpha1/": { + "get": { + "tags": ["API Discovery"], + "description": "get available 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/dashboard.grafana.app/v0alpha1/librarypanels": { + "get": { + "tags": ["LibraryPanel"], + "description": "list objects of kind LibraryPanel", + "operationId": "listLibraryPanelForAllNamespaces", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "LibraryPanel" + } + }, + "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": "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/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/dashboards": { + "get": { + "tags": ["Dashboard"], + "description": "list or watch objects of kind Dashboard", + "operationId": "listDashboard", + "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.pkg.apis.dashboard.v0alpha1.DashboardList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "post": { + "tags": ["Dashboard"], + "description": "create a Dashboard", + "operationId": "createDashboard", + "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": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "delete": { + "tags": ["Dashboard"], + "description": "delete collection of Dashboard", + "operationId": "deletecollectionDashboard", + "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 + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "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": "Dashboard" + } + }, + "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}/dashboards/{name}": { + "get": { + "tags": ["Dashboard"], + "description": "read the specified Dashboard", + "operationId": "getDashboard", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "put": { + "tags": ["Dashboard"], + "description": "replace the specified Dashboard", + "operationId": "replaceDashboard", + "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": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "delete": { + "tags": ["Dashboard"], + "description": "delete a Dashboard", + "operationId": "deleteDashboard", + "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 + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "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": "Dashboard" + } + }, + "patch": { + "tags": ["Dashboard"], + "description": "partially update the specified Dashboard", + "operationId": "updateDashboard", + "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.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Dashboard", + "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}/dashboards/{name}/dto": { + "get": { + "tags": ["Dashboard"], + "description": "connect GET requests to dto of Dashboard", + "operationId": "getDashboardDto", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardWithAccessInfo" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "DashboardWithAccessInfo" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the DashboardWithAccessInfo", + "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": "path", + "in": "query", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "version", + "in": "query", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/librarypanels": { + "get": { + "tags": ["LibraryPanel"], + "description": "list objects of kind LibraryPanel", + "operationId": "listLibraryPanel", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "LibraryPanel" + } + }, + "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/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.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanel" + } + } + } + } + }, + "x-kubernetes-action": "get", + "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": ["Search"], + "description": "Dashboard search", + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "workspace", + "required": true, + "schema": { + "type": "string" + }, + "example": "default" + }, + { + "name": "query", + "in": "query", + "description": "user query string", + "schema": { + "type": "string" + } + }, + { + "name": "folder", + "in": "query", + "description": "search/list within a folder (not recursive)", + "schema": { + "type": "string" + } + }, + { + "name": "sort", + "in": "query", + "description": "sortable field", + "schema": { + "type": "string" + }, + "examples": { + "": { + "summary": "default sorting" + }, + "-title": { + "summary": "title descending", + "value": "-title" + }, + "title": { + "summary": "title ascending", + "value": "title" + } + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["totalHits", "hits"], + "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" + }, + "facets": { + "description": "Facet results", + "type": "object", + "additionalProperties": { + "default": {} + } + }, + "hits": { + "description": "The dashboard body (unstructured for now)", + "type": "array", + "items": { + "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" + }, + "maxScore": { + "description": "Max score", + "type": "number", + "format": "double" + }, + "offset": { + "description": "Where the query started from", + "type": "integer", + "format": "int64" + }, + "queryCost": { + "description": "Cost of running the query", + "type": "number", + "format": "double" + }, + "sortBy": { + "description": "How are the results sorted" + }, + "totalHits": { + "description": "The number of matching results", + "type": "integer", + "format": "int64", + "default": 0 + } + } + } + } + } + } + } + } + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/search/sortable": { + "get": { + "tags": ["Search"], + "description": "Get sortable fields", + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "workspace", + "required": true, + "schema": { + "type": "string" + }, + "example": "default" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["fields"], + "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" + }, + "fields": { + "description": "Sortable fields (depends on backend support)", + "type": "array", + "items": { + "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" + } + } + } + } + } + } + } + } + }, + "/apis/dashboard.grafana.app/v0alpha1/search": null, + "/apis/dashboard.grafana.app/v0alpha1/watch/namespaces/{namespace}/dashboards": { + "get": { + "tags": ["Dashboard"], + "description": "watch individual changes to a list of Dashboard. deprecated: use the 'watch' parameter with a list operation instead.", + "operationId": "watchDashboardList", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + } + } + }, + "x-kubernetes-action": "watchlist", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "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/dashboard.grafana.app/v0alpha1/watch/namespaces/{namespace}/dashboards/{name}": { + "get": { + "tags": ["Dashboard"], + "description": "watch changes to an object of kind Dashboard. deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter.", + "operationId": "watchDashboard", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + } + } + }, + "x-kubernetes-action": "watch", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "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": "name", + "in": "path", + "description": "name of the Dashboard", + "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 + } + }, + { + "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 + } + } + ] + } + }, + "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", + "examples": ["now-1h"] + }, + "to": { + "description": "To is the end time of the query.", + "type": "string", + "default": "now", + "examples": ["now"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true, + "$schema": "https://json-schema.org/draft-04/schema" + }, + "com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataSourceRef": { + "type": "object", + "additionalProperties": true + }, + "com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured": { + "type": "object", + "additionalProperties": true, + "x-kubernetes-preserve-unknown-fields": true + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.AnnotationActions": { + "type": "object", + "required": ["canAdd", "canEdit", "canDelete"], + "properties": { + "canAdd": { + "type": "boolean", + "default": false + }, + "canDelete": { + "type": "boolean", + "default": false + }, + "canEdit": { + "type": "boolean", + "default": false + } + } + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.AnnotationPermission": { + "type": "object", + "required": ["dashboard", "organization"], + "properties": { + "dashboard": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.AnnotationActions" + } + ] + }, + "organization": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.AnnotationActions" + } + ] + } + } + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard": { + "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": "The dashboard body (unstructured for now)", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "Dashboard", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardAccess": { + "description": "Information about how the requesting user can use a given dashboard", + "type": "object", + "required": ["canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"], + "properties": { + "annotationsPermissions": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.AnnotationPermission" + }, + "canAdmin": { + "type": "boolean", + "default": false + }, + "canDelete": { + "type": "boolean", + "default": false + }, + "canEdit": { + "type": "boolean", + "default": false + }, + "canSave": { + "description": "The permissions part", + "type": "boolean", + "default": false + }, + "canStar": { + "type": "boolean", + "default": false + }, + "slug": { + "description": "Metadata fields", + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardList": { + "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" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.Dashboard" + } + ] + } + }, + "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": "DashboardList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardWithAccessInfo": { + "description": "This is like the legacy DTO where access and metadata are all returned in a single call", + "type": "object", + "required": ["spec", "access"], + "properties": { + "access": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.DashboardAccess" + } + ] + }, + "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": "The dashboard body (unstructured for now)", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "DashboardWithAccessInfo", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.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.pkg.apis.dashboard.v0alpha1.LibraryPanelSpec" + } + ] + }, + "status": { + "description": "Status will show errors", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelStatus" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "LibraryPanel", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.dashboard.v0alpha1.LibraryPanelList": { + "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" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.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.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" + } + ] + }, + "options": { + "description": "The options schema depends on the panel type", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + ] + }, + "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 panel title", + "type": "string" + }, + "type": { + "description": "The panel type", + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.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": "" + } + } + } + }, + "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" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent": { + "description": "Event represents a single event to a watched resource.", + "type": "object", + "required": ["type", "object"], + "properties": { + "object": { + "description": "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension" + } + ] + }, + "type": { + "type": "string", + "default": "" + } + } + }, + "io.k8s.apimachinery.pkg.runtime.RawExtension": { + "description": "RawExtension is used to hold extensions in external versions.\n\nTo use this, make a field which has RawExtension as its type in your external, versioned struct, and Object in your internal struct. You also need to register your various plugin types.\n\n// Internal package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.Object `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// External package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.RawExtension `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// On the wire, the JSON will look something like this:\n\n\t{\n\t\t\"kind\":\"MyAPIObject\",\n\t\t\"apiVersion\":\"v1\",\n\t\t\"myPlugin\": {\n\t\t\t\"kind\":\"PluginA\",\n\t\t\t\"aOption\":\"foo\",\n\t\t},\n\t}\n\nSo what happens? Decode first uses json or yaml to unmarshal the serialized data into your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. The next step is to copy (using pkg/conversion) into the internal struct. The runtime package's DefaultScheme has conversion functions installed which will unpack the JSON stored in RawExtension, turning it into the correct object type, and storing it in the Object. (TODO: In the case where the object is of an unknown type, a runtime.Unknown object will be created and stored.)", + "type": "object" + } + } + } +} diff --git a/openapi/folder.grafana.app-v0alpha1.json b/openapi/folder.grafana.app-v0alpha1.json new file mode 100644 index 00000000000..426d681c6bd --- /dev/null +++ b/openapi/folder.grafana.app-v0alpha1.json @@ -0,0 +1,1764 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Grafana folders", + "title": "folder.grafana.app/v0alpha1" + }, + "paths": { + "/apis/folder.grafana.app/v0alpha1/": { + "get": { + "tags": ["API Discovery"], + "description": "get available 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/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders": { + "get": { + "tags": ["Folder"], + "description": "list objects of kind Folder", + "operationId": "listFolder", + "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.pkg.apis.folder.v0alpha1.FolderList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "post": { + "tags": ["Folder"], + "description": "create a Folder", + "operationId": "createFolder", + "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": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "delete": { + "tags": ["Folder"], + "description": "delete collection of Folder", + "operationId": "deletecollectionFolder", + "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 + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "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": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "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/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders/{name}": { + "get": { + "tags": ["Folder"], + "description": "read the specified Folder", + "operationId": "getFolder", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "put": { + "tags": ["Folder"], + "description": "replace the specified Folder", + "operationId": "replaceFolder", + "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": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "delete": { + "tags": ["Folder"], + "description": "delete a Folder", + "operationId": "deleteFolder", + "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 + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "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": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "patch": { + "tags": ["Folder"], + "description": "partially update the specified Folder", + "operationId": "updateFolder", + "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.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "Folder" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Folder", + "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/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders/{name}/access": { + "get": { + "tags": ["Folder"], + "description": "connect GET requests to access of Folder", + "operationId": "getFolderAccess", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderAccessInfo" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "FolderAccessInfo" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the FolderAccessInfo", + "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 + } + } + ] + }, + "/apis/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders/{name}/counts": { + "get": { + "tags": ["Folder"], + "description": "connect GET requests to counts of Folder", + "operationId": "getFolderCounts", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.DescendantCounts" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "DescendantCounts" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the DescendantCounts", + "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 + } + } + ] + }, + "/apis/folder.grafana.app/v0alpha1/namespaces/{namespace}/folders/{name}/parents": { + "get": { + "tags": ["Folder"], + "description": "connect GET requests to parents of Folder", + "operationId": "getFolderParents", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderInfoList" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v0alpha1", + "kind": "FolderInfoList" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the FolderInfoList", + "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 + } + } + ] + } + }, + "components": { + "schemas": { + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.DescendantCounts": { + "type": "object", + "required": ["counts"], + "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" + }, + "counts": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.ResourceStats" + } + ] + } + }, + "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" + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "folder.grafana.app", + "kind": "DescendantCounts", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "DescendantCounts", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder": { + "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" + }, + "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": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Spec" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "folder.grafana.app", + "kind": "Folder", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "Folder", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderAccessInfo": { + "description": "Access control information for the current user", + "type": "object", + "required": ["canSave", "canEdit", "canAdmin", "canDelete"], + "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" + }, + "canAdmin": { + "type": "boolean", + "default": false + }, + "canDelete": { + "type": "boolean", + "default": false + }, + "canEdit": { + "type": "boolean", + "default": false + }, + "canSave": { + "type": "boolean", + "default": false + }, + "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" + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "folder.grafana.app", + "kind": "FolderAccessInfo", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "FolderAccessInfo", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderInfo": { + "description": "FolderInfo briefly describes a folder -- unlike a folder resource, this is a partial record of the folder metadata used for navigating parents and children", + "type": "object", + "required": ["name", "title"], + "properties": { + "description": { + "description": "The folder description", + "type": "string" + }, + "detached": { + "description": "This folder does not resolve", + "type": "boolean" + }, + "name": { + "description": "Name is the k8s name (eg, the unique identifier) for a folder", + "type": "string", + "default": "" + }, + "parent": { + "description": "The parent folder UID", + "type": "string" + }, + "title": { + "description": "Title is the display value", + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderInfoList": { + "description": "FolderInfoList returns a list of folder references (parents or children) Unlike FolderList, each item is not a full k8s 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" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderInfo" + } + ] + }, + "x-kubernetes-list-map-keys": ["uid"], + "x-kubernetes-list-type": "map" + }, + "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": "folder.grafana.app", + "kind": "FolderInfoList", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "FolderInfoList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.FolderList": { + "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" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Folder" + } + ] + } + }, + "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": "folder.grafana.app", + "kind": "FolderList", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "FolderList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.ResourceStats": { + "type": "object", + "required": ["group", "resource", "count"], + "properties": { + "count": { + "type": "integer", + "format": "int64", + "default": 0 + }, + "group": { + "type": "string", + "default": "" + }, + "resource": { + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.pkg.apis.folder.v0alpha1.Spec": { + "type": "object", + "required": ["title"], + "properties": { + "description": { + "description": "Describe the feature toggle", + "type": "string" + }, + "title": { + "description": "Describe the feature toggle", + "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" + } + } + } +} diff --git a/openapi/peakq.grafana.app-v0alpha1.json b/openapi/peakq.grafana.app-v0alpha1.json new file mode 100644 index 00000000000..b931b96dde8 --- /dev/null +++ b/openapi/peakq.grafana.app-v0alpha1.json @@ -0,0 +1,2523 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "peakq.grafana.app/v0alpha1" + }, + "paths": { + "/apis/peakq.grafana.app/v0alpha1/": { + "get": { + "description": "get available 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/peakq.grafana.app/v0alpha1/namespaces/{namespace}/querytemplates": { + "get": { + "tags": ["QueryTemplate"], + "description": "list or watch objects of kind QueryTemplate", + "operationId": "listQueryTemplate", + "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.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "post": { + "tags": ["QueryTemplate"], + "description": "create a QueryTemplate", + "operationId": "createQueryTemplate", + "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": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "delete": { + "tags": ["QueryTemplate"], + "description": "delete collection of QueryTemplate", + "operationId": "deletecollectionQueryTemplate", + "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 + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "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": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "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/peakq.grafana.app/v0alpha1/namespaces/{namespace}/querytemplates/{name}": { + "get": { + "tags": ["QueryTemplate"], + "description": "read the specified QueryTemplate", + "operationId": "getQueryTemplate", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "put": { + "tags": ["QueryTemplate"], + "description": "replace the specified QueryTemplate", + "operationId": "replaceQueryTemplate", + "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": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "delete": { + "tags": ["QueryTemplate"], + "description": "delete a QueryTemplate", + "operationId": "deleteQueryTemplate", + "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 + } + } + ], + "requestBody": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + } + } + }, + "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": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "patch": { + "tags": ["QueryTemplate"], + "description": "partially update the specified QueryTemplate", + "operationId": "updateQueryTemplate", + "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.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the QueryTemplate", + "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/peakq.grafana.app/v0alpha1/namespaces/{namespace}/querytemplates/{name}/render": { + "get": { + "tags": ["QueryTemplate"], + "description": "connect GET requests to render of QueryTemplate", + "operationId": "getQueryTemplateRender", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "RenderedQuery" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the RenderedQuery", + "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 + } + } + ] + }, + "/apis/peakq.grafana.app/v0alpha1/querytemplates": { + "get": { + "tags": ["QueryTemplate"], + "description": "list or watch objects of kind QueryTemplate", + "operationId": "listQueryTemplateForAllNamespaces", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "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": "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/peakq.grafana.app/v0alpha1/render": { + "summary": "an example at the root level", + "description": "longer description here?", + "post": { + "parameters": [ + { + "name": "variables", + "in": "query", + "description": "Each variable is prefixed with var-{variable}={value}", + "style": "form", + "explode": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "var-another": ["first", "second"], + "var-metricName": ["up"] + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {}, + "examples": { + "test": { + "summary": "hello", + "value": { + "title": "Test", + "vars": [ + { + "key": "metricName", + "defaultValues": ["down"] + } + ], + "targets": [ + { + "variables": { + "metricName": [ + { + "path": "$.expr", + "position": { + "start": 0, + "end": 10 + } + }, + { + "path": "$.expr", + "position": { + "start": 13, + "end": 23 + } + } + ] + }, + "properties": { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "foo" + }, + "editorMode": "builder", + "expr": "metricName + metricName + 42", + "instant": true, + "range": false, + "exemplar": false + } + } + ] + } + }, + "test2": { + "summary": "hello2", + "value": { + "title": "Test", + "vars": [ + { + "key": "metricName", + "defaultValues": ["down"] + } + ], + "targets": [ + { + "variables": { + "metricName": [ + { + "path": "$.expr", + "position": { + "start": 0, + "end": 10 + } + }, + { + "path": "$.expr", + "position": { + "start": 13, + "end": 23 + } + } + ] + }, + "properties": { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "foo" + }, + "instant": true, + "range": false, + "exemplar": false, + "editorMode": "builder", + "expr": "metricName + metricName + 42" + } + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "description": "Dummy object that represents a real query 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" + }, + "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" + }, + "targets": { + "type": "array", + "items": { + "default": {} + }, + "x-kubernetes-list-type": "atomic" + } + } + } + } + } + } + } + } + }, + "/apis/peakq.grafana.app/v0alpha1/watch/namespaces/{namespace}/querytemplates": { + "get": { + "tags": ["QueryTemplate"], + "description": "watch individual changes to a list of QueryTemplate. deprecated: use the 'watch' parameter with a list operation instead.", + "operationId": "watchQueryTemplateList", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + } + } + }, + "x-kubernetes-action": "watchlist", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "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/peakq.grafana.app/v0alpha1/watch/namespaces/{namespace}/querytemplates/{name}": { + "get": { + "tags": ["QueryTemplate"], + "description": "watch changes to an object of kind QueryTemplate. deprecated: use the 'watch' parameter with a list operation instead, filtered to a single item with the 'fieldSelector' parameter.", + "operationId": "watchQueryTemplate", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + } + } + }, + "x-kubernetes-action": "watch", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "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": "name", + "in": "path", + "description": "name of the QueryTemplate", + "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 + } + }, + { + "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/peakq.grafana.app/v0alpha1/watch/querytemplates": { + "get": { + "tags": ["QueryTemplate"], + "description": "watch individual changes to a list of QueryTemplate. deprecated: use the 'watch' parameter with a list operation instead.", + "operationId": "watchQueryTemplateListForAllNamespaces", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + } + } + } + }, + "x-kubernetes-action": "watchlist", + "x-kubernetes-group-version-kind": { + "group": "peakq.grafana.app", + "version": "v0alpha1", + "kind": "QueryTemplate" + } + }, + "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": "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 + } + } + ] + } + }, + "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", + "examples": ["now-1h"] + }, + "to": { + "description": "To is the end time of the query.", + "type": "string", + "default": "now", + "examples": ["now"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": true, + "$schema": "https://json-schema.org/draft-04/schema" + }, + "com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured": { + "type": "object", + "additionalProperties": true, + "x-kubernetes-preserve-unknown-fields": true + }, + "com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate": { + "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" + }, + "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": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.QueryTemplate" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "peakq.grafana.app", + "kind": "QueryTemplate", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplateList": { + "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" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.peakq.v0alpha1.QueryTemplate" + } + ] + } + }, + "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": "peakq.grafana.app", + "kind": "QueryTemplateList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Position": { + "description": "Position is where to do replacement in the targets during render.", + "type": "object", + "required": ["start", "end"], + "properties": { + "end": { + "description": "End is the byte offset of the end of the variable.", + "type": "integer", + "format": "int64", + "default": 0 + }, + "start": { + "description": "Start is the byte offset within TargetKey's property of the variable. It is the start location for replacements).", + "type": "integer", + "format": "int64", + "default": 0 + } + } + }, + "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.QueryTemplate": { + "type": "object", + "required": ["targets"], + "properties": { + "description": { + "description": "Longer description for why it is interesting", + "type": "string" + }, + "targets": { + "description": "Output variables", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Target" + } + ] + }, + "x-kubernetes-list-type": "set" + }, + "title": { + "description": "A display name", + "type": "string" + }, + "vars": { + "description": "The variables that can be used to render", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.TemplateVariable" + } + ] + }, + "x-kubernetes-list-map-keys": ["key"], + "x-kubernetes-list-type": "map" + } + } + }, + "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Target": { + "type": "object", + "required": ["variables", "properties"], + "properties": { + "dataType": { + "description": "DataType is the returned Dataplane type from the query.", + "type": "string" + }, + "properties": { + "description": "Query target", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataQuery" + } + ] + }, + "variables": { + "description": "Variables that will be replaced in the query", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.VariableReplacement" + } + ] + } + } + } + } + }, + "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.TemplateVariable": { + "description": "TemplateVariable is the definition of a variable that will be interpolated in targets.", + "type": "object", + "required": ["key"], + "properties": { + "defaultValues": { + "description": "DefaultValue is the value to be used when there is no selected value during render.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "key": { + "description": "Key is the name of the variable.", + "type": "string", + "default": "" + }, + "valueListDefinition": { + "description": "ValueListDefinition is the object definition used by the FE to get a list of possible values to select for render.", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" + } + ] + } + } + }, + "com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.VariableReplacement": { + "description": "QueryVariable is the definition of a variable that will be interpolated in targets.", + "type": "object", + "required": ["path"], + "properties": { + "format": { + "description": "How values should be interpolated\n\nPossible enum values:\n - `\"csv\"` Formats variables with multiple values as a comma-separated string.\n - `\"doublequote\"` Formats single- and multi-valued variables into a comma-separated string\n - `\"json\"` Formats variables with multiple values as a comma-separated string.\n - `\"pipe\"` Formats variables with multiple values into a pipe-separated string.\n - `\"raw\"` Formats variables with multiple values into comma-separated string. This is the default behavior when no format is specified\n - `\"singlequote\"` Formats single- and multi-valued variables into a comma-separated string", + "type": "string", + "enum": ["csv", "doublequote", "json", "pipe", "raw", "singlequote"] + }, + "path": { + "description": "Path is the location of the property within a target. The format for this is not figured out yet (Maybe JSONPath?). Idea: [\"string\", int, \"string\"] where int indicates array offset", + "type": "string", + "default": "" + }, + "position": { + "description": "Positions is a list of where to perform the interpolation within targets during render. The first string is the Idx of the target as a string, since openAPI does not support ints as map keys", + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.query.v0alpha1.template.Position" + } + ] + } + } + }, + "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" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent": { + "description": "Event represents a single event to a watched resource.", + "type": "object", + "required": ["type", "object"], + "properties": { + "object": { + "description": "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension" + } + ] + }, + "type": { + "type": "string", + "default": "" + } + } + }, + "io.k8s.apimachinery.pkg.runtime.RawExtension": { + "description": "RawExtension is used to hold extensions in external versions.\n\nTo use this, make a field which has RawExtension as its type in your external, versioned struct, and Object in your internal struct. You also need to register your various plugin types.\n\n// Internal package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.Object `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// External package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.RawExtension `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// On the wire, the JSON will look something like this:\n\n\t{\n\t\t\"kind\":\"MyAPIObject\",\n\t\t\"apiVersion\":\"v1\",\n\t\t\"myPlugin\": {\n\t\t\t\"kind\":\"PluginA\",\n\t\t\t\"aOption\":\"foo\",\n\t\t},\n\t}\n\nSo what happens? Decode first uses json or yaml to unmarshal the serialized data into your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. The next step is to copy (using pkg/conversion) into the internal struct. The runtime package's DefaultScheme has conversion functions installed which will unpack the JSON stored in RawExtension, turning it into the correct object type, and storing it in the Object. (TODO: In the case where the object is of an unknown type, a runtime.Unknown object will be created and stored.)", + "type": "object" + } + } + } +} diff --git a/package.json b/package.json index f21f651d5ab..8d523b8b453 100644 --- a/package.json +++ b/package.json @@ -223,7 +223,7 @@ "react-select-event": "5.5.1", "redux-mock-store": "1.5.5", "rimraf": "6.0.1", - "rudder-sdk-js": "2.48.43", + "rudder-sdk-js": "2.48.44", "sass": "1.83.4", "sass-loader": "16.0.4", "smtp-tester": "^2.1.0", @@ -355,7 +355,7 @@ "nanoid": "^5.0.4", "node-forge": "^1.3.1", "ol": "7.4.0", - "ol-ext": "4.0.24", + "ol-ext": "4.0.25", "pluralize": "^8.0.0", "prismjs": "1.29.0", "rc-slider": "11.1.8", @@ -387,7 +387,7 @@ "react-virtual": "2.10.4", "react-virtualized-auto-sizer": "1.0.25", "react-window": "1.8.11", - "react-window-infinite-loader": "1.0.9", + "react-window-infinite-loader": "1.0.10", "react-zoom-pan-pinch": "^3.3.0", "reduce-reducers": "^1.0.4", "redux": "5.0.1", diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 683bdb2a59a..04d84ee202e 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -154,6 +154,7 @@ export interface FeatureToggles { jitterAlertRulesWithinGroups?: boolean; onPremToCloudMigrations?: boolean; alertingSaveStatePeriodic?: boolean; + alertingSaveStateCompressed?: boolean; scopeApi?: boolean; promQLScope?: boolean; logQLScope?: boolean; diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx index 2fa3bde2bd3..9d3bc5fbfb8 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx @@ -6,9 +6,10 @@ import React, { useEffect, useState } from 'react'; import { Alert } from '../Alert/Alert'; import { Field } from '../Forms/Field'; -import { Combobox, ComboboxOption, ComboboxProps } from './Combobox'; +import { Combobox, ComboboxProps } from './Combobox'; import mdx from './Combobox.mdx'; import { fakeSearchAPI, generateOptions } from './storyUtils'; +import { ComboboxOption } from './types'; type PropsAndCustomArgs = ComboboxProps & { numberOfOptions: number; diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx index ab9e63fbb3b..7787e38d00c 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx @@ -2,7 +2,8 @@ import { act, render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { Combobox, ComboboxOption } from './Combobox'; +import { Combobox } from './Combobox'; +import { ComboboxOption } from './types'; // Mock data for the Combobox options const options: ComboboxOption[] = [ diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index 34645d76aca..cf59c4910f8 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -16,15 +16,10 @@ import { ScrollContainer } from '../ScrollContainer/ScrollContainer'; import { AsyncError, NotFoundError } from './MessageRows'; import { fuzzyFind, itemToString } from './filter'; import { getComboboxStyles, MENU_OPTION_HEIGHT, MENU_OPTION_HEIGHT_DESCRIPTION } from './getComboboxStyles'; +import { ComboboxOption } from './types'; import { useComboboxFloat } from './useComboboxFloat'; import { StaleResultError, useLatestAsyncCall } from './useLatestAsyncCall'; -export type ComboboxOption = { - label?: string; - value: T; - description?: string; -}; - // TODO: It would be great if ComboboxOption["label"] was more generic so that if consumers do pass it in (for async), // then the onChange handler emits ComboboxOption with the label as non-undefined. export interface ComboboxBaseProps diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx index e247536c8ce..0dc4f2e9128 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.internal.story.tsx @@ -2,9 +2,9 @@ import { action } from '@storybook/addon-actions'; import { useArgs, useEffect, useState } from '@storybook/preview-api'; import type { Meta, StoryFn, StoryObj } from '@storybook/react'; -import { ComboboxOption } from './Combobox'; import { MultiCombobox } from './MultiCombobox'; import { generateOptions } from './storyUtils'; +import { ComboboxOption } from './types'; const meta: Meta = { title: 'Forms/MultiCombobox', diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index 8099fb595d6..0cede71af9e 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -14,18 +14,17 @@ import { Spinner } from '../Spinner/Spinner'; import { Text } from '../Text/Text'; import { Tooltip } from '../Tooltip'; -import { ComboboxOption, ComboboxBaseProps, AutoSizeConditionals, VIRTUAL_OVERSCAN_ITEMS } from './Combobox'; +import { ComboboxBaseProps, AutoSizeConditionals, VIRTUAL_OVERSCAN_ITEMS } from './Combobox'; import { NotFoundError } from './MessageRows'; import { OptionListItem } from './OptionListItem'; import { ValuePill } from './ValuePill'; import { itemFilter, itemToString } from './filter'; import { getComboboxStyles, MENU_OPTION_HEIGHT, MENU_OPTION_HEIGHT_DESCRIPTION } from './getComboboxStyles'; import { getMultiComboboxStyles } from './getMultiComboboxStyles'; +import { ALL_OPTION_VALUE, ComboboxOption } from './types'; import { useComboboxFloat } from './useComboboxFloat'; import { MAX_SHOWN_ITEMS, useMeasureMulti } from './useMeasureMulti'; -export const ALL_OPTION_VALUE = '__GRAFANA_INTERNAL_MULTICOMBOBOX_ALL_OPTION__'; - interface MultiComboboxBaseProps extends Omit, 'value' | 'onChange'> { value?: T[] | Array>; onChange: (items?: T[]) => void; diff --git a/packages/grafana-ui/src/components/Combobox/filter.ts b/packages/grafana-ui/src/components/Combobox/filter.ts index 94419efe9ca..cb7a14d86fe 100644 --- a/packages/grafana-ui/src/components/Combobox/filter.ts +++ b/packages/grafana-ui/src/components/Combobox/filter.ts @@ -1,7 +1,6 @@ import uFuzzy from '@leeoniya/ufuzzy'; -import { ComboboxOption } from './Combobox'; -import { ALL_OPTION_VALUE } from './MultiCombobox'; +import { ALL_OPTION_VALUE, ComboboxOption } from './types'; // https://catonmat.net/my-favorite-regex :) const REGEXP_NON_ASCII = /[^ -~]/m; diff --git a/packages/grafana-ui/src/components/Combobox/storyUtils.ts b/packages/grafana-ui/src/components/Combobox/storyUtils.ts index 809ac047cb1..48563324f54 100644 --- a/packages/grafana-ui/src/components/Combobox/storyUtils.ts +++ b/packages/grafana-ui/src/components/Combobox/storyUtils.ts @@ -1,4 +1,4 @@ -import { ComboboxOption } from './Combobox'; +import { ComboboxOption } from './types'; let fakeApiOptions: Array>; export async function fakeSearchAPI(urlString: string): Promise>> { diff --git a/packages/grafana-ui/src/components/Combobox/types.ts b/packages/grafana-ui/src/components/Combobox/types.ts new file mode 100644 index 00000000000..c942d7a6357 --- /dev/null +++ b/packages/grafana-ui/src/components/Combobox/types.ts @@ -0,0 +1,7 @@ +export const ALL_OPTION_VALUE = '__GRAFANA_INTERNAL_MULTICOMBOBOX_ALL_OPTION__'; + +export type ComboboxOption = { + label?: string; + value: T; + description?: string; +}; diff --git a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts index 23433b6560a..83eb2631a62 100644 --- a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts +++ b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts @@ -3,7 +3,6 @@ import { useMemo, useRef, useState } from 'react'; import { measureText } from '../../utils'; -import { ComboboxOption } from './Combobox'; import { MENU_ITEM_FONT_SIZE, MENU_ITEM_FONT_WEIGHT, @@ -11,6 +10,7 @@ import { MENU_OPTION_HEIGHT, POPOVER_MAX_HEIGHT, } from './getComboboxStyles'; +import { ComboboxOption } from './types'; // Only consider the first n items when calculating the width of the popover. const WIDTH_CALCULATION_LIMIT_ITEMS = 100_000; diff --git a/packages/grafana-ui/src/components/Combobox/useMeasureMulti.ts b/packages/grafana-ui/src/components/Combobox/useMeasureMulti.ts index 4ff65a7d41f..f64bd007307 100644 --- a/packages/grafana-ui/src/components/Combobox/useMeasureMulti.ts +++ b/packages/grafana-ui/src/components/Combobox/useMeasureMulti.ts @@ -3,7 +3,7 @@ import { useMeasure } from 'react-use'; import { measureText } from '../../utils'; -import { ComboboxOption } from './Combobox'; +import { ComboboxOption } from './types'; const FONT_SIZE = 12; const EXTRA_PILL_SIZE = 50; diff --git a/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx index 5e98db356b8..bd047f8be99 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/WeekStartPicker.tsx @@ -3,7 +3,8 @@ import { useCallback, useMemo } from 'react'; import { BootData } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Combobox, ComboboxOption } from '../Combobox/Combobox'; +import { Combobox } from '../Combobox/Combobox'; +import { ComboboxOption } from '../Combobox/types'; export interface Props { onChange: (weekStart: string) => void; diff --git a/packages/grafana-ui/src/components/DateTimePickers/options.ts b/packages/grafana-ui/src/components/DateTimePickers/options.ts index d96209d2bf4..8f64046cf41 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/options.ts +++ b/packages/grafana-ui/src/components/DateTimePickers/options.ts @@ -1,6 +1,6 @@ import { TimeOption } from '@grafana/data'; -import { ComboboxOption } from '../Combobox/Combobox'; +import { ComboboxOption } from '../Combobox/types'; export const quickOptions: TimeOption[] = [ { from: 'now-5m', to: 'now', display: 'Last 5 minutes' }, diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index fc76cf7eabd..900190b9acf 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -239,7 +239,8 @@ export { SelectMenuOptions } from './Select/SelectMenu'; export { getSelectStyles } from './Select/getSelectStyles'; export * from './Select/types'; -export { Combobox, type ComboboxOption } from './Combobox/Combobox'; +export { Combobox } from './Combobox/Combobox'; +export { type ComboboxOption } from './Combobox/types'; export { HorizontalGroup, VerticalGroup, Container } from './Layout/Layout'; export { Badge, type BadgeColor, type BadgeProps } from './Badge/Badge'; diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index c3a712a8811..999586b13a1 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" dashboard "github.com/grafana/grafana/pkg/apis/dashboard" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/dashboards" @@ -54,8 +55,9 @@ type dashboardSqlAccess struct { provisioning provisioning.ProvisioningService // Use for writing (not reading) - dashStore dashboards.Store - softDelete bool + dashStore dashboards.Store + softDelete bool + dashboardSearchClient legacysearcher.DashboardSearchClient // Typically one... the server wrapper subscribers []chan *resource.WrittenEvent @@ -68,12 +70,14 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, provisioning provisioning.ProvisioningService, softDelete bool, ) DashboardAccess { + dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore) return &dashboardSqlAccess{ - sql: sql, - namespacer: namespacer, - dashStore: dashStore, - provisioning: provisioning, - softDelete: softDelete, + sql: sql, + namespacer: namespacer, + dashStore: dashStore, + provisioning: provisioning, + softDelete: softDelete, + dashboardSearchClient: *dashboardSearchClient, } } diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index 0f88fdf15a5..e5f51bf78f9 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "strings" "time" claims "github.com/grafana/authlib/types" @@ -255,9 +254,8 @@ func (a *dashboardSqlAccess) Read(ctx context.Context, req *resource.ReadRequest return a.ReadResource(ctx, req), nil } -// TODO: this needs to be implemented func (a *dashboardSqlAccess) Search(ctx context.Context, req *resource.ResourceSearchRequest) (*resource.ResourceSearchResponse, error) { - return nil, fmt.Errorf("not yet (filter)") + return a.dashboardSearchClient.Search(ctx, req) } func (a *dashboardSqlAccess) ListRepositoryObjects(ctx context.Context, req *resource.ListRepositoryObjectsRequest) (*resource.ListRepositoryObjectsResponse, error) { @@ -270,35 +268,5 @@ func (a *dashboardSqlAccess) CountRepositoryObjects(context.Context, *resource.C // GetStats implements ResourceServer. func (a *dashboardSqlAccess) GetStats(ctx context.Context, req *resource.ResourceStatsRequest) (*resource.ResourceStatsResponse, error) { - info, err := claims.ParseNamespace(req.Namespace) - if err != nil { - return nil, fmt.Errorf("unable to read namespace") - } - if info.OrgID == 0 { - return nil, fmt.Errorf("invalid OrgID found in namespace") - } - - if len(req.Kinds) != 1 { - return nil, fmt.Errorf("only can query for dashboard kind in legacy fallback") - } - - parts := strings.SplitN(req.Kinds[0], "/", 2) - if len(parts) != 2 { - return nil, fmt.Errorf("invalid kind") - } - - count, err := a.dashStore.CountInOrg(ctx, info.OrgID) - if err != nil { - return nil, err - } - - return &resource.ResourceStatsResponse{ - Stats: []*resource.ResourceStatsResponse_Stats{ - { - Group: parts[0], - Resource: parts[1], - Count: count, - }, - }, - }, nil + return a.dashboardSearchClient.GetStats(ctx, req) } diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go new file mode 100644 index 00000000000..ac221ba86e2 --- /dev/null +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -0,0 +1,128 @@ +package legacysearcher + +import ( + "context" + "fmt" + "strings" + + claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "google.golang.org/grpc" +) + +type DashboardSearchClient struct { + resource.ResourceIndexClient + dashboardStore dashboards.Store +} + +func NewDashboardSearchClient(dashboardStore dashboards.Store) *DashboardSearchClient { + return &DashboardSearchClient{dashboardStore: dashboardStore} +} + +func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) { + user, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + + if req.Query == "*" { + req.Query = "" + } + + // TODO add missing support for the following query params: + // - tag + // - starred (won't support) + // - page (check) + // - type + // - sort + // - deleted + // - permission + // - dashboardIds + // - dashboardUIDs + // - folderIds + // - folderUIDs + // - sort (default by title) + query := &dashboards.FindPersistedDashboardsQuery{ + Title: req.Query, + Limit: req.Limit, + // FolderUIDs: req.FolderUIDs, + SignedInUser: user, + } + + // TODO need to test this + // emptyResponse, err := a.dashService.GetSharedDashboardUIDsQuery(ctx, query) + + // if err != nil { + // return nil, err + // } else if emptyResponse { + // return nil, nil + // } + + res, err := c.dashboardStore.FindDashboards(ctx, query) + if err != nil { + return nil, err + } + + // TODO sort if query.Sort == "" see sortedHits in services/search/service.go + + searchFields := resource.StandardSearchFields() + list := &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + searchFields.Field(resource.SEARCH_FIELD_TITLE), + searchFields.Field(resource.SEARCH_FIELD_FOLDER), + // searchFields.Field(resource.SEARCH_FIELD_TAGS), + }, + }, + } + + for _, dashboard := range res { + list.Results.Rows = append(list.Results.Rows, &resource.ResourceTableRow{ + Key: &resource.ResourceKey{ + Namespace: "default", + Group: "dashboard.grafana.app", + Resource: "dashboards", + Name: dashboard.UID, + }, + Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID)}, // TODO add tag + }) + } + + return list, nil +} + +func (c *DashboardSearchClient) GetStats(ctx context.Context, req *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) { + info, err := claims.ParseNamespace(req.Namespace) + if err != nil { + return nil, fmt.Errorf("unable to read namespace") + } + if info.OrgID == 0 { + return nil, fmt.Errorf("invalid OrgID found in namespace") + } + + if len(req.Kinds) != 1 { + return nil, fmt.Errorf("only can query for dashboard kind in legacy fallback") + } + + parts := strings.SplitN(req.Kinds[0], "/", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid kind") + } + + count, err := c.dashboardStore.CountInOrg(ctx, info.OrgID) + if err != nil { + return nil, err + } + + return &resource.ResourceStatsResponse{ + Stats: []*resource.ResourceStatsResponse_Stats{ + { + Group: parts[0], + Resource: parts[1], + Count: count, + }, + }, + }, nil +} diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 91d9217ed98..7f866943cbe 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apiserver/builder" dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/util/errhttp" ) @@ -32,9 +33,10 @@ type SearchHandler struct { tracer trace.Tracer } -func NewSearchHandler(client resource.ResourceIndexClient, tracer trace.Tracer) *SearchHandler { +func NewSearchHandler(client resource.ResourceIndexClient, tracer trace.Tracer, cfg *setting.Cfg, legacyDashboardSearcher resource.ResourceIndexClient) *SearchHandler { + searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, client, legacyDashboardSearcher) return &SearchHandler{ - client: client, + client: searchClient, log: log.New("grafana-apiserver.dashboards.search"), tracer: tracer, } @@ -332,7 +334,6 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { searchRequest.Options.Fields = append(searchRequest.Options.Fields, namesFilter...) } - // Run the query result, err := s.client.Search(ctx, searchRequest) if err != nil { errhttp.Write(ctx, err, w) diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index 03dcd6cd9d5..23d7ad18278 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -7,13 +7,173 @@ import ( "testing" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "google.golang.org/grpc" ) +func TestSearchFallback(t *testing.T) { + t.Run("should hit legacy search handler on mode 0", func(t *testing.T) { + mockClient := &MockClient{} + mockLegacyClient := &MockClient{} + + cfg := &setting.Cfg{ + UnifiedStorage: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode0}, + }, + } + searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest != nil { + t.Fatalf("expected Search NOT to be called, but it was") + } + if mockLegacyClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + }) + + t.Run("should hit legacy search handler on mode 1", func(t *testing.T) { + mockClient := &MockClient{} + mockLegacyClient := &MockClient{} + + cfg := &setting.Cfg{ + UnifiedStorage: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode1}, + }, + } + searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest != nil { + t.Fatalf("expected Search NOT to be called, but it was") + } + if mockLegacyClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + }) + + t.Run("should hit legacy search handler on mode 2", func(t *testing.T) { + mockClient := &MockClient{} + mockLegacyClient := &MockClient{} + + cfg := &setting.Cfg{ + UnifiedStorage: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode2}, + }, + } + searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest != nil { + t.Fatalf("expected Search NOT to be called, but it was") + } + if mockLegacyClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + }) + + t.Run("should hit unified storage search handler on mode 3", func(t *testing.T) { + mockClient := &MockClient{} + mockLegacyClient := &MockClient{} + + cfg := &setting.Cfg{ + UnifiedStorage: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode3}, + }, + } + searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + if mockLegacyClient.LastSearchRequest != nil { + t.Fatalf("expected Search NOT to be called, but it was") + } + }) + + t.Run("should hit unified storage search handler on mode 4", func(t *testing.T) { + mockClient := &MockClient{} + mockLegacyClient := &MockClient{} + + cfg := &setting.Cfg{ + UnifiedStorage: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode4}, + }, + } + searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + if mockLegacyClient.LastSearchRequest != nil { + t.Fatalf("expected Search NOT to be called, but it was") + } + }) + + t.Run("should hit unified storage search handler on mode 5", func(t *testing.T) { + mockClient := &MockClient{} + mockLegacyClient := &MockClient{} + + cfg := &setting.Cfg{ + UnifiedStorage: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode5}, + }, + } + searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient) + + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search", nil) + req.Header.Add("content-type", "application/json") + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"})) + + searchHandler.DoSearch(rr, req) + + if mockClient.LastSearchRequest == nil { + t.Fatalf("expected Search to be called, but it was not") + } + if mockLegacyClient.LastSearchRequest != nil { + t.Fatalf("expected Search NOT to be called, but it was") + } + }) +} + func TestSearchHandlerFields(t *testing.T) { // Create a mock client mockClient := &MockClient{} diff --git a/pkg/registry/apis/dashboard/v0alpha1/register.go b/pkg/registry/apis/dashboard/v0alpha1/register.go index a6d05ec2de0..535cb0cb08b 100644 --- a/pkg/registry/apis/dashboard/v0alpha1/register.go +++ b/pkg/registry/apis/dashboard/v0alpha1/register.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -71,6 +72,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) dbp := legacysql.NewDatabaseProvider(sql) namespacer := request.GetNamespaceMapper(cfg) + legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore) builder := &DashboardsAPIBuilder{ log: log.New("grafana-apiserver.dashboards.v0alpha1"), DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{ @@ -80,7 +82,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, features: features, accessControl: accessControl, unified: unified, - search: dashboard.NewSearchHandler(unified, tracing), + search: dashboard.NewSearchHandler(unified, tracing, cfg, legacyDashboardSearcher), legacy: &dashboard.DashboardStorage{ Resource: dashboardv0alpha1.DashboardResourceInfo, diff --git a/pkg/services/apiserver/client/client.go b/pkg/services/apiserver/client/client.go index 8d054fc7a9b..2e25ca07ee2 100644 --- a/pkg/services/apiserver/client/client.go +++ b/pkg/services/apiserver/client/client.go @@ -12,8 +12,11 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher" "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" k8sUser "k8s.io/apiserver/pkg/authentication/user" k8sRequest "k8s.io/apiserver/pkg/endpoints/request" @@ -40,12 +43,14 @@ type k8sHandler struct { searcher resource.ResourceIndexClient } -func NewK8sHandler(namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient) K8sHandler { +func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient, dashStore dashboards.Store) K8sHandler { + legacySearcher := legacysearcher.NewDashboardSearchClient(dashStore) + searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, searcher, legacySearcher) return &k8sHandler{ namespacer: namespacer, gvr: gvr, restConfigProvider: restConfigProvider, - searcher: searcher, + searcher: searchClient, } } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 9bde512b504..257d2e23a07 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -12,6 +12,11 @@ import ( "time" "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -43,11 +48,8 @@ import ( secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes" secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" ) func Test_NoopServiceDoesNothing(t *testing.T) { @@ -874,7 +876,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool, cfgOverrides ...conf cfg, featureToggles, nil, nil, rr, sqlStore, kvStore, nil, nil, quotatest.New(false, nil), secretsService, nil, alertMetrics, mockFolder, fakeAccessControl, dashboardService, nil, bus, fakeAccessControlService, annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, - httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), + httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), usertest.NewUserServiceFake(), ) require.NoError(t, err) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 0f2957ee0ce..0c90a37542c 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -91,7 +91,7 @@ func ProvideDashboardServiceImpl( restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient, quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper, ) (*DashboardServiceImpl, error) { - k8sHandler := client.NewK8sHandler(request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified) + k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore) dashSvc := &DashboardServiceImpl{ cfg: cfg, @@ -1074,7 +1074,9 @@ func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query * return nil, err } - if len(result) != 1 { + if len(result) == 0 { + return nil, dashboards.ErrDashboardNotFound + } else if len(result) > 1 { return nil, fmt.Errorf("unexpected number of dashboards found: %d. desired: 1", len(result)) } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index b0ca965e45d..a717b3bb707 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1027,6 +1027,13 @@ var ( FrontendOnly: false, Owner: grafanaAlertingSquad, }, + { + Name: "alertingSaveStateCompressed", + Description: "Enables the compressed protobuf-based alert state storage", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaAlertingSquad, + }, { Name: "scopeApi", Description: "In-development feature flag for the scope api using the app platform.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 169683aea81..5a23134e827 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -135,6 +135,7 @@ newFolderPicker,experimental,@grafana/grafana-frontend-platform,false,false,true jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false onPremToCloudMigrations,preview,@grafana/grafana-operator-experience-squad,false,false,false alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false +alertingSaveStateCompressed,experimental,@grafana/alerting-squad,false,false,false scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false promQLScope,GA,@grafana/oss-big-tent,false,false,false logQLScope,privatePreview,@grafana/observability-logs,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 9a82b4d86a9..6ca1cd23fef 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -551,6 +551,10 @@ const ( // Writes the state periodically to the database, asynchronous to rule evaluation FlagAlertingSaveStatePeriodic = "alertingSaveStatePeriodic" + // FlagAlertingSaveStateCompressed + // Enables the compressed protobuf-based alert state storage + FlagAlertingSaveStateCompressed = "alertingSaveStateCompressed" + // FlagScopeApi // In-development feature flag for the scope api using the app platform. FlagScopeApi = "scopeApi" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 23615673bd0..0ac61ba717c 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -339,6 +339,21 @@ "expression": "false" } }, + { + "metadata": { + "name": "alertingSaveStateCompressed", + "resourceVersion": "1737472824047", + "creationTimestamp": "2025-01-17T18:17:20Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-01-21 15:20:24.047499 +0000 UTC" + } + }, + "spec": { + "description": "Enables the compressed protobuf-based alert state storage", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" + } + }, { "metadata": { "name": "alertingSaveStatePeriodic", diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index e07f3a08470..8b23f58cf5e 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -36,16 +36,6 @@ SELECT DISTINCT , (SELECT COUNT(connection_id) FROM ` + model.LibraryElementConnectionTableName + ` WHERE element_id = le.id AND kind=1) AS connected_dashboards` ) -// redundant SELECT to trick mysql's optimizer -const deleteInvalidConnections = ` -DELETE FROM library_element_connection -WHERE connection_id IN ( - SELECT connection_id FROM ( - SELECT connection_id as id FROM library_element_connection - WHERE element_id=? AND connection_id NOT IN (SELECT id as connection_id from dashboard) - ) as dummy -)` - func getFromLibraryElementDTOWithMeta(dialect migrator.Dialect) string { user := dialect.Quote("user") userJoin := ` @@ -89,15 +79,12 @@ func syncFieldsWithModel(libraryElement *model.LibraryElement) error { return nil } -func GetLibraryElement(dialect migrator.Dialect, session *db.Session, uid string, orgID int64) (model.LibraryElementWithMeta, error) { +func (l *LibraryElementService) GetLibraryElement(c context.Context, signedInUser identity.Requester, session *db.Session, uid string) (model.LibraryElementWithMeta, error) { elements := make([]model.LibraryElementWithMeta, 0) sql := selectLibraryElementDTOWithMeta + - ", coalesce(dashboard.title, 'General') AS folder_name" + - ", coalesce(dashboard.uid, '') AS folder_uid" + - getFromLibraryElementDTOWithMeta(dialect) + - " LEFT JOIN dashboard AS dashboard ON dashboard.id = le.folder_id" + + getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect()) + " WHERE le.uid=? AND le.org_id=?" - sess := session.SQL(sql, uid, orgID) + sess := session.SQL(sql, uid, signedInUser.GetOrgID()) err := sess.Find(&elements) if err != nil { return model.LibraryElementWithMeta{}, err @@ -109,6 +96,19 @@ func GetLibraryElement(dialect migrator.Dialect, session *db.Session, uid string return model.LibraryElementWithMeta{}, fmt.Errorf("found %d elements, while expecting at most one", len(elements)) } + // get the folder title + f, err := l.folderService.Get(c, &folder.GetFolderQuery{ + OrgID: elements[0].OrgID, + UID: &elements[0].FolderUID, + SignedInUser: signedInUser, + }) + if err == nil { + elements[0].FolderName = f.Title + } else { + // default to General if we cannot find the folder + elements[0].FolderName = "General" + } + return elements[0], nil } @@ -230,7 +230,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) { var elementID int64 err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error { - element, err := GetLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID()) + element, err := l.GetLibraryElement(c, signedInUser, session, uid) if err != nil { return err } @@ -243,11 +243,37 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn } } - // Delete any hanging/invalid connections - if _, err = session.Exec(deleteInvalidConnections, element.ID); err != nil { + dashboardIDs := []int64{} + // get all connections for this element + if err := session.SQL("SELECT connection_id FROM library_element_connection where element_id = ?", element.ID).Find(&dashboardIDs); err != nil { return err } + // then find the dashboards that were supposed to be connected to this element + _, requester := identity.WithServiceIdentitiy(c, signedInUser.GetOrgID()) + dashs, err := l.dashboardsService.FindDashboards(c, &dashboards.FindPersistedDashboardsQuery{ + OrgId: signedInUser.GetOrgID(), + DashboardIds: dashboardIDs, + SignedInUser: requester, // a user may be able to delete a library element but not read all dashboards. We still need to run this check, so we don't allow deleting elements if dashboards are connected + }) + if err != nil { + return err + } + + foundDashes := make([]int64, len(dashs)) + for i, d := range dashs { + foundDashes[i] = d.ID + } + + // delete any connections that are orphaned for this element (i.e. the dashboard was deleted) + session.Table("library_element_connection") + session.Where("element_id = ?", element.ID) + session.NotIn("connection_id", foundDashes) + if _, err = session.Delete(model.LibraryElementConnectionWithMeta{}); err != nil { + return err + } + + // now try to delete the element, but fail if it is connected to any dashboards var connectionIDs []struct { ConnectionID int64 `xorm:"connection_id"` } @@ -577,7 +603,7 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU return model.LibraryElementDTO{}, err } err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error { - elementInDB, err := GetLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID()) + elementInDB, err := l.GetLibraryElement(c, signedInUser, session, uid) if err != nil { return err } @@ -594,7 +620,7 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU return model.ErrLibraryElementUIDTooLong } - _, err := GetLibraryElement(l.SQLStore.GetDialect(), session, updateUID, signedInUser.GetOrgID()) + _, err := l.GetLibraryElement(c, signedInUser, session, updateUID) if !errors.Is(err, model.ErrLibraryElementNotFound) { return model.ErrLibraryElementAlreadyExists } @@ -689,7 +715,7 @@ func (l *LibraryElementService) getConnections(c context.Context, signedInUser i } err = l.SQLStore.WithDbSession(c, func(session *db.Session) error { - element, err := GetLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.GetOrgID()) + element, err := l.GetLibraryElement(c, signedInUser, session, uid) if err != nil { return err } @@ -722,6 +748,9 @@ func (l *LibraryElementService) getConnections(c context.Context, signedInUser i } ds, err := l.dashboardsService.GetDashboardUIDByID(c, &dashboards.GetDashboardRefByIDQuery{ID: connection.ConnectionID}) if err != nil { + if errors.Is(err, dashboards.ErrDashboardNotFound) { + continue + } return err } connections = append(connections, model.LibraryElementConnectionDTO{ @@ -813,7 +842,7 @@ func (l *LibraryElementService) connectElementsToDashboardID(c context.Context, return err } for _, elementUID := range elementUIDs { - element, err := GetLibraryElement(l.SQLStore.GetDialect(), session, elementUID, signedInUser.GetOrgID()) + element, err := l.GetLibraryElement(c, signedInUser, session, elementUID) if err != nil { return err } diff --git a/pkg/services/libraryelements/libraryelements_delete_test.go b/pkg/services/libraryelements/libraryelements_delete_test.go index 597787d6ee0..4246b0ac66d 100644 --- a/pkg/services/libraryelements/libraryelements_delete_test.go +++ b/pkg/services/libraryelements/libraryelements_delete_test.go @@ -82,4 +82,14 @@ func TestDeleteLibraryElement(t *testing.T) { resp := sc.service.deleteHandler(sc.reqContext) require.Equal(t, 403, resp.Status()) }) + + scenarioWithPanel(t, "When an admin tries to delete a library panel that is connected to a non-existent dashboard, it should succeed", + func(t *testing.T, sc scenarioContext) { + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 9999999) + require.NoError(t, err) + + sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) + resp := sc.service.deleteHandler(sc.reqContext) + require.Equal(t, 200, resp.Status()) + }) } diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go index b441505639b..96f64f4c35f 100644 --- a/pkg/services/libraryelements/libraryelements_get_test.go +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -1,12 +1,15 @@ package libraryelements import ( + "encoding/json" "testing" "github.com/google/go-cmp/cmp" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/kinds/librarypanel" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/web" @@ -91,6 +94,37 @@ func TestGetLibraryElement(t *testing.T) { } }) + scenarioWithPanel(t, "When an admin tries to get a library panel that exists, but the original folder does not, it should succeed and return correct result", + func(t *testing.T, sc scenarioContext) { + b, err := json.Marshal(map[string]string{"test": "test"}) + require.NoError(t, err) + newFolder := createFolder(t, sc, "NewFolder", nil) + sc.reqContext.SignedInUser.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll} + sc.reqContext.SignedInUser.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersDelete] = []string{dashboards.ScopeFoldersAll} + result, err := sc.service.createLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{ + FolderID: newFolder.ID, // nolint:staticcheck + FolderUID: &newFolder.UID, + Name: "Testing Library Panel With Deleted Folder", + Kind: 1, + Model: b, + UID: "panel-with-deleted-folder", + }) + require.NoError(t, err) + err = sc.service.folderService.Delete(sc.reqContext.Req.Context(), &folder.DeleteFolderCommand{ + UID: newFolder.UID, + OrgID: sc.reqContext.OrgID, + SignedInUser: sc.reqContext.SignedInUser, + }) + require.NoError(t, err) + err = sc.sqlStore.WithDbSession(sc.reqContext.Req.Context(), func(session *db.Session) error { + elem, err := sc.service.GetLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, session, result.UID) + require.NoError(t, err) + require.Equal(t, elem.FolderName, "General") + return nil + }) + require.NoError(t, err) + }) + scenarioWithPanel(t, "When an admin tries to get a connected library panel, it should succeed and return correct connected dashboards", func(t *testing.T, sc scenarioContext) { dashJSON := map[string]any{ diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index ef2876736d9..7ea77dd22ff 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -32,10 +32,13 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/folderimpl" + "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/libraryelements/model" + ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" @@ -166,6 +169,10 @@ func TestGetLibraryPanelConnections(t *testing.T) { err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) + // add a connection where the dashboard doesn't exist. Shouldn't be returned in the list + err = sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 99999999) + require.NoError(t, err) + var expected = func(res model.LibraryElementConnectionsResponse) model.LibraryElementConnectionsResponse { return model.LibraryElementConnectionsResponse{ Result: []model.LibraryElementConnectionDTO{ @@ -194,6 +201,39 @@ func TestGetLibraryPanelConnections(t *testing.T) { t.Fatalf("Result mismatch (-want +got):\n%s", diff) } }) + + scenarioWithPanel(t, "When an admin tries to create a connection with an element that exists, but the original folder does not, it should still succeed", + func(t *testing.T, sc scenarioContext) { + b, err := json.Marshal(map[string]string{"test": "test"}) + require.NoError(t, err) + newFolder := createFolder(t, sc, "NewFolder", nil) + sc.reqContext.SignedInUser.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll} + sc.reqContext.SignedInUser.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersDelete] = []string{dashboards.ScopeFoldersAll} + _, err = sc.service.createLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{ + FolderID: newFolder.ID, // nolint:staticcheck + FolderUID: &newFolder.UID, + Name: "Testing Library Panel With Deleted Folder", + Kind: 1, + Model: b, + UID: "panel-with-deleted-folder", + }) + require.NoError(t, err) + err = sc.service.folderService.Delete(sc.reqContext.Req.Context(), &folder.DeleteFolderCommand{ + UID: newFolder.UID, + OrgID: sc.reqContext.OrgID, + SignedInUser: sc.reqContext.SignedInUser, + }) + require.NoError(t, err) + + dash := dashboards.Dashboard{ + Title: "Testing create element", + Data: simplejson.NewFromAny(map[string]any{}), + } + // nolint:staticcheck + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID) + err = sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) + require.NoError(t, err) + }) } type libraryElement struct { @@ -473,9 +513,15 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo dashboardPermissions := acmock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) fStore := folderimpl.ProvideStore(sqlStore) + publicDash := &publicdashboards.FakePublicDashboardServiceWrapper{} + publicDash.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) folderSvc := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest()) + nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), publicDash, cfg, nil, tracing.InitializeTracerForTest()) + alertStore, err := ngstore.ProvideDBStore(cfg, features, sqlStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) + require.NoError(t, err) + err = folderSvc.RegisterService(alertStore) + require.NoError(t, err) dashService, dashSvcErr := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, ac, diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 55d29355952..811edf6e893 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -78,6 +79,7 @@ type API struct { Historian Historian Tracer tracing.Tracer AppUrl *url.URL + UserService user.Service // Hooks can be used to replace API handlers for specific paths. Hooks *Hooks @@ -135,6 +137,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { amConfigStore: api.AlertingStore, amRefresher: api.MultiOrgAlertmanager, featureManager: api.FeatureManager, + userService: api.UserService, }, ), m) api.RegisterTestingApiEndpoints(NewTestingApi( diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 935cf1bc8d3..06805da5b19 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "regexp" "strings" "github.com/grafana/grafana/pkg/api/response" @@ -19,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/util" + alertmanager_config "github.com/prometheus/alertmanager/config" ) const disableProvenanceHeaderName = "X-Disable-Provenance" @@ -600,10 +602,67 @@ func escapeAlertingFileExport(body definitions.AlertingFileExport) definitions.A for i, group := range body.Groups { body.Groups[i] = escapeRuleGroup(group) } - // TODO: implement escaping for the other export fields + for i, cp := range body.ContactPoints { + body.ContactPoints[i] = escapeContactPoint(cp) + } + for i, np := range body.Policies { + body.Policies[i] = escapeNotificationPolicy(np) + } return body } +func escapeRouteExport(r *definitions.RouteExport) { + r.Receiver = addEscapeCharactersToString(r.Receiver) + if r.GroupByStr != nil { + groupByStr := make([]string, len(*r.GroupByStr)) + for i, groupBy := range *r.GroupByStr { + groupByStr[i] = addEscapeCharactersToString(groupBy) + } + r.GroupByStr = &groupByStr + } + for k, v := range r.Match { + r.Match[k] = addEscapeCharactersToString(v) + } + for k, v := range r.MatchRE { + // convert regex to string, escape then covert back to regex + stringRepr := addEscapeCharactersToString(v.String()) + mutated := regexp.MustCompile(stringRepr) + r.MatchRE[k] = alertmanager_config.Regexp{Regexp: mutated} + } + if r.MuteTimeIntervals != nil { + muteTimeIntervals := make([]string, len(*r.MuteTimeIntervals)) + for i, muteTimeInterval := range *r.MuteTimeIntervals { + muteTimeIntervals[i] = addEscapeCharactersToString(muteTimeInterval) + } + r.MuteTimeIntervals = &muteTimeIntervals + } + for i := range r.Routes { + escapeRouteExport(r.Routes[i]) + } +} + +func escapeNotificationPolicy(np definitions.NotificationPolicyExport) definitions.NotificationPolicyExport { + escapeRouteExport(np.RouteExport) + return np +} + +func escapeContactPoint(cp definitions.ContactPointExport) definitions.ContactPointExport { + cp.Name = addEscapeCharactersToString(cp.Name) + for i, receiver := range cp.Receivers { + settingsJson, err := receiver.Settings.MarshalJSON() + if err != nil { + // This should never happen, as the settings are already marshaled to JSON in the API + panic(fmt.Errorf("failed to marshal settings to JSON: %w", err)) + } + settingsEscaped := []byte(addEscapeCharactersToString(string(settingsJson))) + if err := cp.Receivers[i].Settings.UnmarshalJSON(settingsEscaped); err != nil { + // This should never happen, as the settings are already marshaled to JSON in the API + panic(fmt.Errorf("failed to unmarshal settings from JSON: %w", err)) + } + } + return cp +} + // escape all strings except: // Alert rule annotations: groups[].rules[].annotations // Alert rule time range: groups[].rules[].relativeTimeRange diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 55783cfe853..f0cc114d77a 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -53,6 +54,7 @@ type RulerSrv struct { cfg *setting.UnifiedAlertingSettings conditionValidator ConditionValidator authz RuleAccessControlService + userService user.Service amConfigStore AMConfigStore amRefresher AMRefresher @@ -211,7 +213,7 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *contextmodel.ReqContext, nam result := apimodels.NamespaceConfigResponse{} for groupKey, rules := range ruleGroups { - result[namespace.Fullpath] = append(result[namespace.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords)) + result[namespace.Fullpath] = append(result[namespace.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords, srv.resolveUserIdToNameFn(c.Req.Context()))) } return response.JSON(http.StatusAccepted, result) @@ -246,7 +248,7 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *contextmodel.ReqContext, namespa result := apimodels.RuleGroupConfigResponse{ // nolint:staticcheck - GettableRuleGroupConfig: toGettableRuleGroupConfig(finalRuleGroup, rules, provenanceRecords), + GettableRuleGroupConfig: toGettableRuleGroupConfig(finalRuleGroup, rules, provenanceRecords, srv.resolveUserIdToNameFn(c.Req.Context())), } return response.JSON(http.StatusAccepted, result) } @@ -300,7 +302,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *contextmodel.ReqContext) response.Res srv.log.Error("Namespace not visible to the user", "user", id, "userNamespace", userNamespace, "namespace", groupKey.NamespaceUID) continue } - result[folder.Fullpath] = append(result[folder.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords)) + result[folder.Fullpath] = append(result[folder.Fullpath], toGettableRuleGroupConfig(groupKey.RuleGroup, rules, provenanceRecords, srv.resolveUserIdToNameFn(c.Req.Context()))) } return response.JSON(http.StatusOK, result) } @@ -323,7 +325,7 @@ func (srv RulerSrv) RouteGetRuleByUID(c *contextmodel.ReqContext, ruleUID string return response.ErrOrFallback(http.StatusInternalServerError, "failed to get rule provenance", err) } - result := toGettableExtendedRuleNode(rule, map[string]ngmodels.Provenance{rule.ResourceID(): provenance}) + result := toGettableExtendedRuleNode(rule, map[string]ngmodels.Provenance{rule.ResourceID(): provenance}, srv.resolveUserIdToNameFn(ctx)) return response.JSON(http.StatusOK, result) } @@ -533,7 +535,7 @@ func changesToResponse(finalChanges *store.GroupDelta) response.Response { return response.JSON(http.StatusAccepted, body) } -func toGettableRuleGroupConfig(groupName string, rules ngmodels.RulesGroup, provenanceRecords map[string]ngmodels.Provenance) apimodels.GettableRuleGroupConfig { +func toGettableRuleGroupConfig(groupName string, rules ngmodels.RulesGroup, provenanceRecords map[string]ngmodels.Provenance, userIdToName userIDToUserInfoFn) apimodels.GettableRuleGroupConfig { rules.SortByGroupIndex() ruleNodes := make([]apimodels.GettableExtendedRuleNode, 0, len(rules)) var interval time.Duration @@ -541,7 +543,7 @@ func toGettableRuleGroupConfig(groupName string, rules ngmodels.RulesGroup, prov interval = time.Duration(rules[0].IntervalSeconds) * time.Second } for _, r := range rules { - ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, provenanceRecords)) + ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, provenanceRecords, userIdToName)) } return apimodels.GettableRuleGroupConfig{ Name: groupName, @@ -550,7 +552,7 @@ func toGettableRuleGroupConfig(groupName string, rules ngmodels.RulesGroup, prov } } -func toGettableExtendedRuleNode(r ngmodels.AlertRule, provenanceRecords map[string]ngmodels.Provenance) apimodels.GettableExtendedRuleNode { +func toGettableExtendedRuleNode(r ngmodels.AlertRule, provenanceRecords map[string]ngmodels.Provenance, userIdToName userIDToUserInfoFn) apimodels.GettableExtendedRuleNode { provenance := ngmodels.ProvenanceNone if prov, exists := provenanceRecords[r.ResourceID()]; exists { provenance = prov @@ -564,6 +566,7 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule, provenanceRecords map[stri Condition: r.Condition, Data: ApiAlertQueriesFromAlertQueries(r.Data), Updated: r.Updated, + UpdatedBy: userIdToName(r.UpdatedBy), IntervalSeconds: r.IntervalSeconds, Version: r.Version, UID: r.UID, @@ -724,3 +727,28 @@ func (srv RulerSrv) searchAuthorizedAlertRules(ctx context.Context, q authorized } return byGroupKey, totalGroups, nil } + +type userIDToUserInfoFn func(id *ngmodels.UserUID) *apimodels.UserInfo + +// getIdentityName returns name of either user or service account +func (srv RulerSrv) resolveUserIdToNameFn(ctx context.Context) userIDToUserInfoFn { + return func(id *ngmodels.UserUID) *apimodels.UserInfo { + if id == nil { + return nil + } + u, err := srv.userService.GetByUID(ctx, &user.GetUserByUIDQuery{ + UID: string(*id), + }) + var result string + if err != nil { + srv.log.FromContext(ctx).Warn("Failed to get user by uid. Defaulting to an empty name", "uid", id, "error", err) + } + if u != nil { + result = u.NameOrFallback() + } + return &apimodels.UserInfo{ + UID: string(*id), + Name: result, + } + } +} diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index ede0a00b799..eec2d83729a 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -32,7 +32,9 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/cmputil" "github.com/grafana/grafana/pkg/web" ) @@ -357,6 +359,72 @@ func TestRouteGetRuleByUID(t *testing.T) { require.Equal(t, expectedRule.Title, result.GrafanaManagedAlert.Title) require.True(t, result.GrafanaManagedAlert.Metadata.EditorSettings.SimplifiedQueryAndExpressionsSection) require.True(t, result.GrafanaManagedAlert.Metadata.EditorSettings.SimplifiedNotificationsSection) + + t.Run("should resolve Updated_by with user service", func(t *testing.T) { + testcases := []struct { + desc string + UpdatedBy *models.UserUID + User *user.User + UserServiceError error + Expected *apimodels.UserInfo + }{ + { + desc: "nil if UpdatedBy is nil", + UpdatedBy: nil, + User: nil, + UserServiceError: nil, + Expected: nil, + }, + { + desc: "just UID if user is not found", + UpdatedBy: util.Pointer(models.UserUID("test-uid")), + User: nil, + UserServiceError: nil, + Expected: &apimodels.UserInfo{ + UID: "test-uid", + }, + }, + { + desc: "just UID if error", + UpdatedBy: util.Pointer(models.UserUID("test-uid")), + UserServiceError: errors.New("error"), + Expected: &apimodels.UserInfo{ + UID: "test-uid", + }, + }, + { + desc: "login if it's known user", + UpdatedBy: util.Pointer(models.UserUID("test-uid")), + User: &user.User{ + Login: "Test", + }, + UserServiceError: nil, + Expected: &apimodels.UserInfo{ + UID: "test-uid", + Name: "Test", + }, + }, + } + for _, tc := range testcases { + t.Run(tc.desc, func(t *testing.T) { + expectedRule.UpdatedBy = tc.UpdatedBy + svc := createService(ruleStore) + usvc := usertest.NewUserServiceFake() + usvc.ExpectedUser = tc.User + usvc.ExpectedError = tc.UserServiceError + svc.userService = usvc + + response := svc.RouteGetRuleByUID(req, expectedRule.UID) + + require.Equal(t, http.StatusOK, response.Status()) + result := &apimodels.GettableExtendedRuleNode{} + require.NoError(t, json.Unmarshal(response.Body(), result)) + require.NotNil(t, result) + + require.Equal(t, tc.Expected, result.GrafanaManagedAlert.UpdatedBy) + }) + } + }) }) t.Run("error when fetching rule with non-existent UID", func(t *testing.T) { @@ -659,6 +727,7 @@ func createService(store *fakes.RuleStore) *RulerSrv { amConfigStore: &fakeAMRefresher{}, amRefresher: &fakeAMRefresher{}, featureManager: featuremgmt.WithFeatures(featuremgmt.FlagGrafanaManagedRecordingRules), + userService: usertest.NewUserServiceFake(), } } diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index a6383efa0b6..502e167f10c 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -364,7 +364,7 @@ const ( type PostableExtendedRuleNode struct { // note: this works with yaml v3 but not v2 (the inline tag isn't accepted on pointers in v2) *ApiRuleNode `yaml:",inline"` - //GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"` + // GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"` GrafanaManagedAlert *PostableGrafanaRule `yaml:"grafana_alert,omitempty" json:"grafana_alert,omitempty"` } @@ -401,7 +401,7 @@ func (n *PostableExtendedRuleNode) validate() error { type GettableExtendedRuleNode struct { // note: this works with yaml v3 but not v2 (the inline tag isn't accepted on pointers in v2) *ApiRuleNode `yaml:",inline"` - //GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"` + // GrafanaManagedAlert yaml.Node `yaml:"grafana_alert,omitempty"` GrafanaManagedAlert *GettableGrafanaRule `yaml:"grafana_alert,omitempty" json:"grafana_alert,omitempty"` } @@ -541,6 +541,7 @@ type GettableGrafanaRule struct { Condition string `json:"condition" yaml:"condition"` Data []AlertQuery `json:"data" yaml:"data"` Updated time.Time `json:"updated" yaml:"updated"` + UpdatedBy *UserInfo `json:"updated_by" yaml:"updated_by"` IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"` Version int64 `json:"version" yaml:"version"` UID string `json:"uid" yaml:"uid"` @@ -555,6 +556,12 @@ type GettableGrafanaRule struct { Metadata *AlertRuleMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` } +// UserInfo represents user-related information, including a unique identifier and a name. +type UserInfo struct { + UID string `json:"uid"` + Name string `json:"name"` +} + // AlertQuery represents a single query associated with an alert definition. type AlertQuery struct { // RefID is the unique identifier of the query, set by the frontend call. diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index e538a14c9b0..2270620dc76 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -49,6 +49,7 @@ import ( "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -78,6 +79,7 @@ func ProvideService( ruleStore *store.DBstore, httpClientProvider httpclient.Provider, resourcePermissions accesscontrol.ReceiverPermissionsService, + userService user.Service, ) (*AlertNG, error) { ng := &AlertNG{ Cfg: cfg, @@ -106,6 +108,7 @@ func ProvideService( store: ruleStore, httpClientProvider: httpClientProvider, ResourcePermissions: resourcePermissions, + userService: userService, } if ng.IsDisabled() { @@ -144,6 +147,7 @@ type AlertNG struct { dashboardService dashboards.DashboardService Api *api.API httpClientProvider httpclient.Provider + InstanceStore state.InstanceStore // Alerting notification services MultiOrgAlertmanager *notifier.MultiOrgAlertmanager @@ -153,6 +157,7 @@ type AlertNG struct { ResourcePermissions accesscontrol.ReceiverPermissionsService annotationsRepo annotations.Repository store *store.DBstore + userService user.Service bus bus.Bus pluginsStore pluginstore.Store @@ -398,11 +403,14 @@ func (ng *AlertNG) init() error { if err != nil { return err } - cfg := state.ManagerCfg{ + + ng.InstanceStore = initInstanceStore(ng.store.SQLStore, ng.Log.New("ngalert.state.instancestore"), ng.FeatureToggles) + + stateManagerCfg := state.ManagerCfg{ Metrics: ng.Metrics.GetStateMetrics(), ExternalURL: appUrl, DisableExecution: !ng.Cfg.UnifiedAlerting.ExecuteAlerts, - InstanceStore: ng.store, + InstanceStore: ng.InstanceStore, Images: ng.ImageService, Clock: clk, Historian: history, @@ -415,13 +423,8 @@ func (ng *AlertNG) init() error { Log: log.New("ngalert.state.manager"), ResolvedRetention: ng.Cfg.UnifiedAlerting.ResolvedAlertRetention, } - logger := log.New("ngalert.state.manager.persist") - statePersister := state.NewSyncStatePersisiter(logger, cfg) - if ng.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingSaveStatePeriodic) { - ticker := clock.New().Ticker(ng.Cfg.UnifiedAlerting.StatePeriodicSaveInterval) - statePersister = state.NewAsyncStatePersister(logger, ticker, cfg) - } - stateManager := state.NewManager(cfg, statePersister) + statePersister := initStatePersister(ng.Cfg.UnifiedAlerting, stateManagerCfg, ng.FeatureToggles) + stateManager := state.NewManager(stateManagerCfg, statePersister) scheduler := schedule.NewScheduler(schedCfg, stateManager) // if it is required to include folder title to the alerts, we need to subscribe to changes of alert title @@ -497,6 +500,7 @@ func (ng *AlertNG) init() error { Historian: history, Hooks: api.NewHooks(ng.Log), Tracer: ng.tracer, + UserService: ng.userService, } ng.Api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics()) @@ -515,6 +519,54 @@ func (ng *AlertNG) init() error { return DeclareFixedRoles(ng.AccesscontrolService, ng.FeatureToggles) } +func initInstanceStore(sqlStore db.DB, logger log.Logger, featureToggles featuremgmt.FeatureToggles) state.InstanceStore { + var instanceStore state.InstanceStore + + if featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingSaveStateCompressed) { + logger.Info("Using protobuf-based alert instance store") + instanceStore = store.ProtoInstanceDBStore{ + SQLStore: sqlStore, + Logger: logger, + FeatureToggles: featureToggles, + } + + // If FlagAlertingSaveStateCompressed is enabled, ProtoInstanceDBStore is used, + // which functions differently from InstanceDBStore. FlagAlertingSaveStatePeriodic is + // not applicable to ProtoInstanceDBStore, so a warning is logged if it is set. + if featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingSaveStatePeriodic) { + logger.Warn("alertingSaveStatePeriodic is not used when alertingSaveStateCompressed feature flag enabled") + } + } else { + logger.Info("Using simple database alert instance store") + instanceStore = store.InstanceDBStore{ + SQLStore: sqlStore, + Logger: logger, + FeatureToggles: featureToggles, + } + } + + return instanceStore +} + +func initStatePersister(uaCfg setting.UnifiedAlertingSettings, cfg state.ManagerCfg, featureToggles featuremgmt.FeatureToggles) state.StatePersister { + logger := log.New("ngalert.state.manager.persist") + var statePersister state.StatePersister + + if featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingSaveStateCompressed) { + logger.Info("Using rule state persister") + statePersister = state.NewSyncRuleStatePersisiter(logger, cfg) + } else if featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingSaveStatePeriodic) { + logger.Info("Using periodic state persister") + ticker := clock.New().Ticker(uaCfg.StatePeriodicSaveInterval) + statePersister = state.NewAsyncStatePersister(logger, ticker, cfg) + } else { + logger.Info("Using sync state persister") + statePersister = state.NewSyncStatePersisiter(logger, cfg) + } + + return statePersister +} + func subscribeToFolderChanges(logger log.Logger, bus bus.Bus, dbStore api.RuleStore) { // if full path to the folder is changed, we update all alert rules in that folder to make sure that all peers (in HA mode) will update folder title and // clean up the current state @@ -553,7 +605,7 @@ func (ng *AlertNG) Run(ctx context.Context) error { // Also note that this runs synchronously to ensure state is loaded // before rule evaluation begins, hence we use ctx and not subCtx. // - ng.stateManager.Warm(ctx, ng.store, ng.store) + ng.stateManager.Warm(ctx, ng.store, ng.InstanceStore) children.Go(func() error { return ng.schedule.Run(subCtx) diff --git a/pkg/services/ngalert/ngalert_test.go b/pkg/services/ngalert/ngalert_test.go index 82542eeba71..2012ff930fc 100644 --- a/pkg/services/ngalert/ngalert_test.go +++ b/pkg/services/ngalert/ngalert_test.go @@ -14,12 +14,16 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" acfakes "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/state" + "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -192,3 +196,94 @@ grafana_alerting_state_history_info{backend="noop"} 0 require.NoError(t, err) }) } + +type mockDB struct { + db.DB +} + +func TestInitInstanceStore(t *testing.T) { + sqlStore := &mockDB{} + logger := log.New() + + tests := []struct { + name string + ft featuremgmt.FeatureToggles + expectedInstanceStoreType interface{} + }{ + { + name: "Compressed flag enabled, no periodic flag", + ft: featuremgmt.WithFeatures( + featuremgmt.FlagAlertingSaveStateCompressed, + ), + expectedInstanceStoreType: store.ProtoInstanceDBStore{}, + }, + { + name: "Compressed flag enabled with periodic flag", + ft: featuremgmt.WithFeatures( + featuremgmt.FlagAlertingSaveStateCompressed, + featuremgmt.FlagAlertingSaveStatePeriodic, + ), + expectedInstanceStoreType: store.ProtoInstanceDBStore{}, + }, + { + name: "Compressed flag disabled", + ft: featuremgmt.WithFeatures(), + expectedInstanceStoreType: store.InstanceDBStore{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + instanceStore := initInstanceStore(sqlStore, logger, tt.ft) + assert.IsType(t, tt.expectedInstanceStoreType, instanceStore) + }) + } +} + +func TestInitStatePersister(t *testing.T) { + ua := setting.UnifiedAlertingSettings{ + StatePeriodicSaveInterval: 1 * time.Minute, + } + cfg := state.ManagerCfg{} + + tests := []struct { + name string + ft featuremgmt.FeatureToggles + expectedStatePersisterType state.StatePersister + }{ + { + name: "Compressed flag enabled", + ft: featuremgmt.WithFeatures( + featuremgmt.FlagAlertingSaveStateCompressed, + ), + expectedStatePersisterType: &state.SyncRuleStatePersister{}, + }, + { + name: "Periodic flag enabled", + ft: featuremgmt.WithFeatures( + featuremgmt.FlagAlertingSaveStatePeriodic, + ), + expectedStatePersisterType: &state.AsyncStatePersister{}, + }, + { + name: "No flags enabled", + ft: featuremgmt.WithFeatures(), + expectedStatePersisterType: &state.SyncStatePersister{}, + }, + { + name: "Both flags enabled - compressed takes precedence", + ft: featuremgmt.WithFeatures( + featuremgmt.FlagAlertingSaveStateCompressed, + featuremgmt.FlagAlertingSaveStatePeriodic, + ), + expectedStatePersisterType: &state.SyncRuleStatePersister{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + statePersister := initStatePersister(ua, cfg, tt.ft) + assert.IsType(t, tt.expectedStatePersisterType, statePersister) + }) + } +} diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index 59b11bce96b..8bc43fee306 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -47,7 +47,7 @@ func TestWarmStateCache(t *testing.T) { evaluationTime, err := time.Parse("2006-01-02", "2021-03-25") require.NoError(t, err) ctx := context.Background() - _, dbstore := tests.SetupTestEnv(t, 1) + ng, dbstore := tests.SetupTestEnv(t, 1) const mainOrgID int64 = 1 rule := tests.CreateTestAlertRule(t, ctx, dbstore, 600, mainOrgID) @@ -216,13 +216,13 @@ func TestWarmStateCache(t *testing.T) { ResultFingerprint: data.Fingerprint(2).String(), }) for _, instance := range instances { - _ = dbstore.SaveAlertInstance(ctx, instance) + _ = ng.InstanceStore.SaveAlertInstance(ctx, instance) } cfg := state.ManagerCfg{ Metrics: metrics.NewNGAlert(prometheus.NewPedanticRegistry()).GetStateMetrics(), ExternalURL: nil, - InstanceStore: dbstore, + InstanceStore: ng.InstanceStore, Images: &state.NoopImageService{}, Clock: clock.NewMock(), Historian: &state.FakeHistorian{}, @@ -230,7 +230,7 @@ func TestWarmStateCache(t *testing.T) { Log: log.New("ngalert.state.manager"), } st := state.NewManager(cfg, state.NewNoopPersister()) - st.Warm(ctx, dbstore, dbstore) + st.Warm(ctx, dbstore, ng.InstanceStore) t.Run("instance cache has expected entries", func(t *testing.T) { for _, entry := range expectedEntries { @@ -250,7 +250,7 @@ func TestDashboardAnnotations(t *testing.T) { require.NoError(t, err) ctx := context.Background() - _, dbstore := tests.SetupTestEnv(t, 1) + ng, dbstore := tests.SetupTestEnv(t, 1) fakeAnnoRepo := annotationstest.NewFakeAnnotationsRepo() historianMetrics := metrics.NewHistorianMetrics(prometheus.NewRegistry(), metrics.Subsystem) @@ -261,7 +261,7 @@ func TestDashboardAnnotations(t *testing.T) { cfg := state.ManagerCfg{ Metrics: metrics.NewNGAlert(prometheus.NewPedanticRegistry()).GetStateMetrics(), ExternalURL: nil, - InstanceStore: dbstore, + InstanceStore: ng.InstanceStore, Images: &state.NoopImageService{}, Clock: clock.New(), Historian: hist, @@ -277,7 +277,7 @@ func TestDashboardAnnotations(t *testing.T) { "test2": "{{ $labels.instance_label }}", }) - st.Warm(ctx, dbstore, dbstore) + st.Warm(ctx, dbstore, ng.InstanceStore) bValue := float64(42) cValue := float64(1) _ = st.ProcessEvalResults(ctx, evaluationTime, rule, eval.Results{{ @@ -1697,7 +1697,7 @@ func TestStaleResultsHandler(t *testing.T) { interval := time.Minute ctx := context.Background() - _, dbstore := tests.SetupTestEnv(t, 1) + ng, dbstore := tests.SetupTestEnv(t, 1) const mainOrgID int64 = 1 rule := tests.CreateTestAlertRule(t, ctx, dbstore, int64(interval.Seconds()), mainOrgID) @@ -1751,7 +1751,7 @@ func TestStaleResultsHandler(t *testing.T) { } for _, instance := range instances { - _ = dbstore.SaveAlertInstance(ctx, instance) + _ = ng.InstanceStore.SaveAlertInstance(ctx, instance) } testCases := []struct { @@ -1805,7 +1805,7 @@ func TestStaleResultsHandler(t *testing.T) { cfg := state.ManagerCfg{ Metrics: metrics.NewNGAlert(prometheus.NewPedanticRegistry()).GetStateMetrics(), ExternalURL: nil, - InstanceStore: dbstore, + InstanceStore: ng.InstanceStore, Images: &state.NoopImageService{}, Clock: clock.New(), Historian: &state.FakeHistorian{}, @@ -1813,7 +1813,7 @@ func TestStaleResultsHandler(t *testing.T) { Log: log.New("ngalert.state.manager"), } st := state.NewManager(cfg, state.NewNoopPersister()) - st.Warm(ctx, dbstore, dbstore) + st.Warm(ctx, dbstore, ng.InstanceStore) existingStatesForRule := st.GetStatesForRuleUID(rule.OrgID, rule.UID) // We have loaded the expected number of entries from the db @@ -1978,7 +1978,7 @@ func TestStaleResults(t *testing.T) { func TestDeleteStateByRuleUID(t *testing.T) { interval := time.Minute ctx := context.Background() - _, dbstore := tests.SetupTestEnv(t, 1) + ng, dbstore := tests.SetupTestEnv(t, 1) const mainOrgID int64 = 1 rule := tests.CreateTestAlertRule(t, ctx, dbstore, int64(interval.Seconds()), mainOrgID) @@ -2009,7 +2009,7 @@ func TestDeleteStateByRuleUID(t *testing.T) { } for _, instance := range instances { - _ = dbstore.SaveAlertInstance(ctx, instance) + _ = ng.InstanceStore.SaveAlertInstance(ctx, instance) } testCases := []struct { @@ -2025,7 +2025,7 @@ func TestDeleteStateByRuleUID(t *testing.T) { }{ { desc: "all states/instances are removed from cache and DB", - instanceStore: dbstore, + instanceStore: ng.InstanceStore, expectedStates: []*state.State{ { AlertRuleUID: rule.UID, @@ -2065,7 +2065,7 @@ func TestDeleteStateByRuleUID(t *testing.T) { cfg := state.ManagerCfg{ Metrics: metrics.NewNGAlert(prometheus.NewPedanticRegistry()).GetStateMetrics(), ExternalURL: nil, - InstanceStore: dbstore, + InstanceStore: ng.InstanceStore, Images: &state.NoopImageService{}, Clock: clk, Historian: &state.FakeHistorian{}, @@ -2073,9 +2073,9 @@ func TestDeleteStateByRuleUID(t *testing.T) { Log: log.New("ngalert.state.manager"), } st := state.NewManager(cfg, state.NewNoopPersister()) - st.Warm(ctx, dbstore, dbstore) + st.Warm(ctx, dbstore, ng.InstanceStore) q := &models.ListAlertInstancesQuery{RuleOrgID: rule.OrgID, RuleUID: rule.UID} - alerts, _ := dbstore.ListAlertInstances(ctx, q) + alerts, _ := ng.InstanceStore.ListAlertInstances(ctx, q) existingStatesForRule := st.GetStatesForRuleUID(rule.OrgID, rule.UID) // We have loaded the expected number of entries from the db @@ -2107,7 +2107,7 @@ func TestDeleteStateByRuleUID(t *testing.T) { } q = &models.ListAlertInstancesQuery{RuleOrgID: rule.OrgID, RuleUID: rule.UID} - alertInstances, _ := dbstore.ListAlertInstances(ctx, q) + alertInstances, _ := ng.InstanceStore.ListAlertInstances(ctx, q) existingStatesForRule = st.GetStatesForRuleUID(rule.OrgID, rule.UID) // The expected number of state entries remains after states are deleted @@ -2120,7 +2120,7 @@ func TestDeleteStateByRuleUID(t *testing.T) { func TestResetStateByRuleUID(t *testing.T) { interval := time.Minute ctx := context.Background() - _, dbstore := tests.SetupTestEnv(t, 1) + ng, dbstore := tests.SetupTestEnv(t, 1) const mainOrgID int64 = 1 rule := tests.CreateTestAlertRule(t, ctx, dbstore, int64(interval.Seconds()), mainOrgID) @@ -2151,7 +2151,7 @@ func TestResetStateByRuleUID(t *testing.T) { } for _, instance := range instances { - _ = dbstore.SaveAlertInstance(ctx, instance) + _ = ng.InstanceStore.SaveAlertInstance(ctx, instance) } testCases := []struct { @@ -2168,7 +2168,7 @@ func TestResetStateByRuleUID(t *testing.T) { }{ { desc: "all states/instances are removed from cache and DB and saved in historian", - instanceStore: dbstore, + instanceStore: ng.InstanceStore, expectedStates: []*state.State{ { AlertRuleUID: rule.UID, @@ -2206,7 +2206,7 @@ func TestResetStateByRuleUID(t *testing.T) { cfg := state.ManagerCfg{ Metrics: metrics.NewNGAlert(prometheus.NewPedanticRegistry()).GetStateMetrics(), ExternalURL: nil, - InstanceStore: dbstore, + InstanceStore: ng.InstanceStore, Images: &state.NoopImageService{}, Clock: clk, Historian: fakeHistorian, @@ -2214,9 +2214,9 @@ func TestResetStateByRuleUID(t *testing.T) { Log: log.New("ngalert.state.manager"), } st := state.NewManager(cfg, state.NewNoopPersister()) - st.Warm(ctx, dbstore, dbstore) + st.Warm(ctx, dbstore, ng.InstanceStore) q := &models.ListAlertInstancesQuery{RuleOrgID: rule.OrgID, RuleUID: rule.UID} - alerts, _ := dbstore.ListAlertInstances(ctx, q) + alerts, _ := ng.InstanceStore.ListAlertInstances(ctx, q) existingStatesForRule := st.GetStatesForRuleUID(rule.OrgID, rule.UID) // We have loaded the expected number of entries from the db @@ -2251,7 +2251,7 @@ func TestResetStateByRuleUID(t *testing.T) { assert.Equal(t, transitions, fakeHistorian.StateTransitions) q = &models.ListAlertInstancesQuery{RuleOrgID: rule.OrgID, RuleUID: rule.UID} - alertInstances, _ := dbstore.ListAlertInstances(ctx, q) + alertInstances, _ := ng.InstanceStore.ListAlertInstances(ctx, q) existingStatesForRule = st.GetStatesForRuleUID(rule.OrgID, rule.UID) // The expected number of state entries remains after states are deleted diff --git a/pkg/services/ngalert/store/instance_database.go b/pkg/services/ngalert/store/instance_database.go index bb11d90a464..6201188c9ff 100644 --- a/pkg/services/ngalert/store/instance_database.go +++ b/pkg/services/ngalert/store/instance_database.go @@ -9,14 +9,21 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/sqlstore" ) +type InstanceDBStore struct { + SQLStore db.DB + Logger log.Logger + FeatureToggles featuremgmt.FeatureToggles +} + // ListAlertInstances is a handler for retrieving alert instances within specific organisation // based on various filters. -func (st DBstore) ListAlertInstances(ctx context.Context, cmd *models.ListAlertInstancesQuery) (result []*models.AlertInstance, err error) { +func (st InstanceDBStore) ListAlertInstances(ctx context.Context, cmd *models.ListAlertInstancesQuery) (result []*models.AlertInstance, err error) { err = st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { alertInstances := make([]*models.AlertInstance, 0) @@ -51,7 +58,7 @@ func (st DBstore) ListAlertInstances(ctx context.Context, cmd *models.ListAlertI } // SaveAlertInstance is a handler for saving a new alert instance. -func (st DBstore) SaveAlertInstance(ctx context.Context, alertInstance models.AlertInstance) error { +func (st InstanceDBStore) SaveAlertInstance(ctx context.Context, alertInstance models.AlertInstance) error { return st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { if err := models.ValidateAlertInstance(alertInstance); err != nil { return err @@ -89,7 +96,7 @@ func (st DBstore) SaveAlertInstance(ctx context.Context, alertInstance models.Al }) } -func (st DBstore) FetchOrgIds(ctx context.Context) ([]int64, error) { +func (st InstanceDBStore) FetchOrgIds(ctx context.Context) ([]int64, error) { orgIds := []int64{} err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { @@ -113,7 +120,7 @@ func (st DBstore) FetchOrgIds(ctx context.Context) ([]int64, error) { } // DeleteAlertInstances deletes instances with the provided keys in a single transaction. -func (st DBstore) DeleteAlertInstances(ctx context.Context, keys ...models.AlertInstanceKey) error { +func (st InstanceDBStore) DeleteAlertInstances(ctx context.Context, keys ...models.AlertInstanceKey) error { if len(keys) == 0 { return nil } @@ -212,12 +219,13 @@ func (st DBstore) DeleteAlertInstances(ctx context.Context, keys ...models.Alert } // SaveAlertInstancesForRule is not implemented for instance database store. -func (st DBstore) SaveAlertInstancesForRule(ctx context.Context, key models.AlertRuleKeyWithGroup, instances []models.AlertInstance) error { +func (st InstanceDBStore) SaveAlertInstancesForRule(ctx context.Context, key models.AlertRuleKeyWithGroup, instances []models.AlertInstance) error { st.Logger.Error("SaveAlertInstancesForRule is not implemented for instance database store.") return errors.New("method SaveAlertInstancesForRule is not implemented for instance database store") } -func (st DBstore) DeleteAlertInstancesByRule(ctx context.Context, key models.AlertRuleKeyWithGroup) error { +// DeleteAlertInstancesByRule deletes all instances for a given rule. +func (st InstanceDBStore) DeleteAlertInstancesByRule(ctx context.Context, key models.AlertRuleKeyWithGroup) error { return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { _, err := sess.Exec("DELETE FROM alert_instance WHERE rule_org_id = ? AND rule_uid = ?", key.OrgID, key.UID) return err @@ -230,7 +238,7 @@ func (st DBstore) DeleteAlertInstancesByRule(ctx context.Context, key models.Ale // // The batchSize parameter controls how many instances are inserted per batch. Increasing batchSize can improve // performance for large datasets, but can also increase load on the database. -func (st DBstore) FullSync(ctx context.Context, instances []models.AlertInstance, batchSize int) error { +func (st InstanceDBStore) FullSync(ctx context.Context, instances []models.AlertInstance, batchSize int) error { if len(instances) == 0 { return nil } @@ -267,7 +275,7 @@ func (st DBstore) FullSync(ctx context.Context, instances []models.AlertInstance }) } -func (st DBstore) insertInstancesBatch(sess *sqlstore.DBSession, batch []models.AlertInstance) error { +func (st InstanceDBStore) insertInstancesBatch(sess *sqlstore.DBSession, batch []models.AlertInstance) error { // If the batch is empty, nothing to insert. if len(batch) == 0 { return nil diff --git a/pkg/services/ngalert/store/instance_database_bench_test.go b/pkg/services/ngalert/store/instance_database_bench_test.go new file mode 100644 index 00000000000..739f052d94d --- /dev/null +++ b/pkg/services/ngalert/store/instance_database_bench_test.go @@ -0,0 +1,105 @@ +package store_test + +import ( + "context" + "flag" + "fmt" + "testing" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/tests" +) + +var saveStateCompressed = flag.Bool("save-state-compressed", false, "Save state compressed") + +func BenchmarkSaveAlertInstances(b *testing.B) { + ctx := context.Background() + + opts := []tests.TestEnvOption{} + if *saveStateCompressed { + opts = append(opts, tests.WithFeatureToggles( + featuremgmt.WithFeatures(featuremgmt.FlagAlertingSaveStateCompressed)), + ) + } + + benchmarkRun := func(b *testing.B, instanceCount, labelCount int) { + ng, dbstore := tests.SetupTestEnv(b, baseIntervalSeconds, opts...) + + const mainOrgID int64 = 1 + + alertRule := tests.CreateTestAlertRule(b, ctx, dbstore, 60, mainOrgID) + + // Create some instances to write down and then delete. + instances := make([]models.AlertInstance, 0, instanceCount) + keys := make([]models.AlertInstanceKey, 0, instanceCount) + for i := 0; i < instanceCount; i++ { + labels := models.InstanceLabels{"instance": fmt.Sprintf("instance-%d", i)} + for li := 0; li < labelCount; li++ { + labels[fmt.Sprintf("label-%d", li)] = fmt.Sprintf("value-%d", li) + } + _, labelsHash, _ := labels.StringAndHash() + + instance := models.AlertInstance{ + AlertInstanceKey: models.AlertInstanceKey{ + RuleOrgID: alertRule.OrgID, + RuleUID: alertRule.UID, + LabelsHash: labelsHash, + }, + CurrentState: models.InstanceStateFiring, + CurrentReason: string(models.InstanceStateError), + Labels: labels, + } + instances = append(instances, instance) + keys = append(keys, instance.AlertInstanceKey) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var err error + + if *saveStateCompressed { + err = ng.InstanceStore.SaveAlertInstancesForRule(ctx, alertRule.GetKeyWithGroup(), instances) + if err != nil { + b.Fatalf("error: %s", err) + } + + // Clean up instances. + b.StopTimer() + err = ng.InstanceStore.DeleteAlertInstancesByRule(ctx, alertRule.GetKeyWithGroup()) + if err != nil { + b.Fatalf("error: %s", err) + } + b.StartTimer() + } else { + for _, instance := range instances { + err = ng.InstanceStore.SaveAlertInstance(ctx, instance) + if err != nil { + b.Fatalf("error: %s", err) + } + } + + // Clean up instances. + b.StopTimer() + err = ng.InstanceStore.DeleteAlertInstances(ctx, keys...) + if err != nil { + b.Fatalf("error: %s", err) + } + b.StartTimer() + } + } + } + + b.Run("100 instances with 10 labels each", func(b *testing.B) { + benchmarkRun(b, 100, 10) + }) + + b.Run("100 instances with 100 labels each", func(b *testing.B) { + benchmarkRun(b, 100, 100) + }) + + b.Run("1000 instances with 10 labels each", func(b *testing.B) { + benchmarkRun(b, 1000, 10) + }) +} diff --git a/pkg/services/ngalert/store/instance_database_test.go b/pkg/services/ngalert/store/instance_database_test.go index 87016b9e50a..cb29fa394e7 100644 --- a/pkg/services/ngalert/store/instance_database_test.go +++ b/pkg/services/ngalert/store/instance_database_test.go @@ -1,57 +1,188 @@ package store_test import ( + "bytes" "context" "fmt" "testing" "time" + "github.com/golang/snappy" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ngalert/models" + pb "github.com/grafana/grafana/pkg/services/ngalert/store/proto/v1" "github.com/grafana/grafana/pkg/services/ngalert/tests" "github.com/grafana/grafana/pkg/util" ) const baseIntervalSeconds = 10 -func BenchmarkAlertInstanceOperations(b *testing.B) { - b.StopTimer() +func TestIntegration_CompressedAlertRuleStateOperations(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + ctx := context.Background() - _, dbstore := tests.SetupTestEnv(b, baseIntervalSeconds) + ng, dbstore := tests.SetupTestEnv( + t, + baseIntervalSeconds, + tests.WithFeatureToggles( + featuremgmt.WithFeatures(featuremgmt.FlagAlertingSaveStateCompressed), + ), + ) const mainOrgID int64 = 1 - alertRule := tests.CreateTestAlertRule(b, ctx, dbstore, 60, mainOrgID) + alertRule1 := tests.CreateTestAlertRule(t, ctx, dbstore, 60, mainOrgID) + orgID := alertRule1.OrgID + alertRule2 := tests.CreateTestAlertRule(t, ctx, dbstore, 60, mainOrgID) + require.Equal(t, orgID, alertRule2.OrgID) - // Create some instances to write down and then delete. - count := 10_003 - instances := make([]models.AlertInstance, 0, count) - keys := make([]models.AlertInstanceKey, 0, count) - for i := 0; i < count; i++ { - labels := models.InstanceLabels{"test": fmt.Sprint(i)} - _, labelsHash, _ := labels.StringAndHash() - instance := models.AlertInstance{ - AlertInstanceKey: models.AlertInstanceKey{ - RuleOrgID: alertRule.OrgID, - RuleUID: alertRule.UID, - LabelsHash: labelsHash, + tests := []struct { + name string + setupInstances func() []models.AlertInstance + listQuery *models.ListAlertInstancesQuery + validate func(t *testing.T, alerts []*models.AlertInstance) + }{ + { + name: "can save and read alert rule state", + setupInstances: func() []models.AlertInstance { + return []models.AlertInstance{ + createAlertInstance(alertRule1.OrgID, alertRule1.UID, "labelsHash1", string(models.InstanceStateError), models.InstanceStateFiring), + } }, - CurrentState: models.InstanceStateFiring, - CurrentReason: string(models.InstanceStateError), - Labels: labels, - } - instances = append(instances, instance) - keys = append(keys, instance.AlertInstanceKey) + listQuery: &models.ListAlertInstancesQuery{ + RuleOrgID: alertRule1.OrgID, + RuleUID: alertRule1.UID, + }, + validate: func(t *testing.T, alerts []*models.AlertInstance) { + require.Len(t, alerts, 1) + require.Equal(t, "labelsHash1", alerts[0].LabelsHash) + }, + }, + { + name: "can save and read alert rule state with multiple instances", + setupInstances: func() []models.AlertInstance { + return []models.AlertInstance{ + createAlertInstance(alertRule1.OrgID, alertRule1.UID, "hash1", "", models.InstanceStateFiring), + createAlertInstance(alertRule1.OrgID, alertRule1.UID, "hash2", "", models.InstanceStateFiring), + } + }, + listQuery: &models.ListAlertInstancesQuery{ + RuleOrgID: alertRule1.OrgID, + RuleUID: alertRule1.UID, + }, + validate: func(t *testing.T, alerts []*models.AlertInstance) { + require.Len(t, alerts, 2) + containsHash(t, alerts, "hash1") + containsHash(t, alerts, "hash2") + }, + }, } - b.StartTimer() - for i := 0; i < b.N; i++ { - for _, instance := range instances { - _ = dbstore.SaveAlertInstance(ctx, instance) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + instances := tc.setupInstances() + err := ng.InstanceStore.SaveAlertInstancesForRule(ctx, alertRule1.GetKeyWithGroup(), instances) + require.NoError(t, err) + alerts, err := ng.InstanceStore.ListAlertInstances(ctx, tc.listQuery) + require.NoError(t, err) + tc.validate(t, alerts) + }) + } +} + +func TestIntegration_CompressedAlertRuleStateOperations_NoNormalState(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + ng, dbstore := tests.SetupTestEnv( + t, + baseIntervalSeconds, + tests.WithFeatureToggles( + featuremgmt.WithFeatures( + featuremgmt.FlagAlertingSaveStateCompressed, + featuremgmt.FlagAlertingNoNormalState, + ), + ), + ) + + const mainOrgID int64 = 1 + + alertRule1 := tests.CreateTestAlertRule(t, ctx, dbstore, 60, mainOrgID) + orgID := alertRule1.OrgID + + tests := []struct { + name string + setupInstances func() []models.AlertInstance + listQuery *models.ListAlertInstancesQuery + validate func(t *testing.T, alerts []*models.AlertInstance) + }{ + { + name: "should ignore Normal state with no reason if feature flag is enabled", + setupInstances: func() []models.AlertInstance { + return []models.AlertInstance{ + createAlertInstance(orgID, util.GenerateShortUID(), util.GenerateShortUID(), "", models.InstanceStateNormal), + createAlertInstance(orgID, util.GenerateShortUID(), "errorHash", "error", models.InstanceStateNormal), + } + }, + listQuery: &models.ListAlertInstancesQuery{ + RuleOrgID: orgID, + }, + validate: func(t *testing.T, alerts []*models.AlertInstance) { + require.Len(t, alerts, 1) + containsHash(t, alerts, "errorHash") + for _, instance := range alerts { + if instance.CurrentState == models.InstanceStateNormal && instance.CurrentReason == "" { + require.Fail(t, "List operation expected to return all states except Normal but the result contains Normal states") + } + } + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + instances := tc.setupInstances() + err := ng.InstanceStore.SaveAlertInstancesForRule(ctx, alertRule1.GetKeyWithGroup(), instances) + require.NoError(t, err) + alerts, err := ng.InstanceStore.ListAlertInstances(ctx, tc.listQuery) + require.NoError(t, err) + tc.validate(t, alerts) + }) + } +} + +// containsHash is a helper function to check if an instance with +// a given labels hash exists in the list of alert instances. +func containsHash(t *testing.T, instances []*models.AlertInstance, hash string) { + t.Helper() + + for _, i := range instances { + if i.LabelsHash == hash { + return } - _ = dbstore.DeleteAlertInstances(ctx, keys...) + } + + require.Fail(t, fmt.Sprintf("%v does not contain an instance with hash %s", instances, hash)) +} + +func createAlertInstance(orgID int64, ruleUID, labelsHash, reason string, state models.InstanceStateType) models.AlertInstance { + return models.AlertInstance{ + AlertInstanceKey: models.AlertInstanceKey{ + RuleOrgID: orgID, + RuleUID: ruleUID, + LabelsHash: labelsHash, + }, + CurrentState: state, + CurrentReason: reason, + Labels: models.InstanceLabels{"label1": "value1"}, } } @@ -60,20 +191,10 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) { t.Skip("skipping integration test") } ctx := context.Background() - _, dbstore := tests.SetupTestEnv(t, baseIntervalSeconds) + ng, dbstore := tests.SetupTestEnv(t, baseIntervalSeconds) const mainOrgID int64 = 1 - containsHash := func(t *testing.T, instances []*models.AlertInstance, hash string) { - t.Helper() - for _, i := range instances { - if i.LabelsHash == hash { - return - } - } - require.Fail(t, "%v does not contain an instance with hash %s", instances, hash) - } - alertRule1 := tests.CreateTestAlertRule(t, ctx, dbstore, 60, mainOrgID) orgID := alertRule1.OrgID @@ -99,14 +220,14 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) { CurrentReason: string(models.InstanceStateError), Labels: labels, } - err := dbstore.SaveAlertInstance(ctx, instance) + err := ng.InstanceStore.SaveAlertInstance(ctx, instance) require.NoError(t, err) listCmd := &models.ListAlertInstancesQuery{ RuleOrgID: instance.RuleOrgID, RuleUID: instance.RuleUID, } - alerts, err := dbstore.ListAlertInstances(ctx, listCmd) + alerts, err := ng.InstanceStore.ListAlertInstances(ctx, listCmd) require.NoError(t, err) require.Len(t, alerts, 1) @@ -128,7 +249,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) { CurrentState: models.InstanceStateNormal, Labels: labels, } - err := dbstore.SaveAlertInstance(ctx, instance) + err := ng.InstanceStore.SaveAlertInstance(ctx, instance) require.NoError(t, err) listCmd := &models.ListAlertInstancesQuery{ @@ -136,7 +257,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) { RuleUID: instance.RuleUID, } - alerts, err := dbstore.ListAlertInstances(ctx, listCmd) + alerts, err := ng.InstanceStore.ListAlertInstances(ctx, listCmd) require.NoError(t, err) require.Len(t, alerts, 1) @@ -158,7 +279,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) { Labels: labels, } - err := dbstore.SaveAlertInstance(ctx, instance1) + err := ng.InstanceStore.SaveAlertInstance(ctx, instance1) require.NoError(t, err) labels = models.InstanceLabels{"test": "testValue2"} @@ -172,7 +293,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) { CurrentState: models.InstanceStateFiring, Labels: labels, } - err = dbstore.SaveAlertInstance(ctx, instance2) + err = ng.InstanceStore.SaveAlertInstance(ctx, instance2) require.NoError(t, err) listQuery := &models.ListAlertInstancesQuery{ @@ -180,7 +301,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) { RuleUID: instance1.RuleUID, } - alerts, err := dbstore.ListAlertInstances(ctx, listQuery) + alerts, err := ng.InstanceStore.ListAlertInstances(ctx, listQuery) require.NoError(t, err) require.Len(t, alerts, 2) @@ -191,13 +312,21 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) { RuleOrgID: orgID, } - alerts, err := dbstore.ListAlertInstances(ctx, listQuery) + alerts, err := ng.InstanceStore.ListAlertInstances(ctx, listQuery) require.NoError(t, err) require.Len(t, alerts, 4) }) t.Run("should ignore Normal state with no reason if feature flag is enabled", func(t *testing.T) { + ng, _ := tests.SetupTestEnv( + t, + baseIntervalSeconds, + tests.WithFeatureToggles( + featuremgmt.WithFeatures(featuremgmt.FlagAlertingNoNormalState), + ), + ) + labels := models.InstanceLabels{"test": util.GenerateShortUID()} instance1 := models.AlertInstance{ AlertInstanceKey: models.AlertInstanceKey{ @@ -219,27 +348,16 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) { CurrentReason: models.StateReasonError, Labels: labels, } - err := dbstore.SaveAlertInstance(ctx, instance1) + err := ng.InstanceStore.SaveAlertInstance(ctx, instance1) require.NoError(t, err) - err = dbstore.SaveAlertInstance(ctx, instance2) + err = ng.InstanceStore.SaveAlertInstance(ctx, instance2) require.NoError(t, err) listQuery := &models.ListAlertInstancesQuery{ RuleOrgID: orgID, } - alerts, err := dbstore.ListAlertInstances(ctx, listQuery) - require.NoError(t, err) - - containsHash(t, alerts, instance1.LabelsHash) - - f := dbstore.FeatureToggles - dbstore.FeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagAlertingNoNormalState) - t.Cleanup(func() { - dbstore.FeatureToggles = f - }) - - alerts, err = dbstore.ListAlertInstances(ctx, listQuery) + alerts, err := ng.InstanceStore.ListAlertInstances(ctx, listQuery) require.NoError(t, err) containsHash(t, alerts, instance2.LabelsHash) @@ -264,7 +382,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) { Labels: labels, } - err := dbstore.SaveAlertInstance(ctx, instance1) + err := ng.InstanceStore.SaveAlertInstance(ctx, instance1) require.NoError(t, err) instance2 := models.AlertInstance{ @@ -276,7 +394,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) { CurrentState: models.InstanceStateNormal, Labels: instance1.Labels, } - err = dbstore.SaveAlertInstance(ctx, instance2) + err = ng.InstanceStore.SaveAlertInstance(ctx, instance2) require.NoError(t, err) listQuery := &models.ListAlertInstancesQuery{ @@ -284,7 +402,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) { RuleUID: alertRule4.UID, } - alerts, err := dbstore.ListAlertInstances(ctx, listQuery) + alerts, err := ng.InstanceStore.ListAlertInstances(ctx, listQuery) require.NoError(t, err) require.Len(t, alerts, 1) @@ -300,7 +418,7 @@ func TestIntegrationFullSync(t *testing.T) { batchSize := 1 ctx := context.Background() - _, dbstore := tests.SetupTestEnv(t, baseIntervalSeconds) + ng, _ := tests.SetupTestEnv(t, baseIntervalSeconds) orgID := int64(1) @@ -312,10 +430,10 @@ func TestIntegrationFullSync(t *testing.T) { } t.Run("Should do a proper full sync", func(t *testing.T) { - err := dbstore.FullSync(ctx, instances, batchSize) + err := ng.InstanceStore.FullSync(ctx, instances, batchSize) require.NoError(t, err) - res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ + res, err := ng.InstanceStore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ RuleOrgID: orgID, }) require.NoError(t, err) @@ -335,10 +453,10 @@ func TestIntegrationFullSync(t *testing.T) { }) t.Run("Should remove non existing entries on sync", func(t *testing.T) { - err := dbstore.FullSync(ctx, instances[1:], batchSize) + err := ng.InstanceStore.FullSync(ctx, instances[1:], batchSize) require.NoError(t, err) - res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ + res, err := ng.InstanceStore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ RuleOrgID: orgID, }) require.NoError(t, err) @@ -352,10 +470,10 @@ func TestIntegrationFullSync(t *testing.T) { t.Run("Should add new entries on sync", func(t *testing.T) { newRuleUID := "y" - err := dbstore.FullSync(ctx, append(instances, generateTestAlertInstance(orgID, newRuleUID)), batchSize) + err := ng.InstanceStore.FullSync(ctx, append(instances, generateTestAlertInstance(orgID, newRuleUID)), batchSize) require.NoError(t, err) - res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ + res, err := ng.InstanceStore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ RuleOrgID: orgID, }) require.NoError(t, err) @@ -377,10 +495,10 @@ func TestIntegrationFullSync(t *testing.T) { t.Run("Should save all instances when batch size is bigger than 1", func(t *testing.T) { batchSize = 2 newRuleUID := "y" - err := dbstore.FullSync(ctx, append(instances, generateTestAlertInstance(orgID, newRuleUID)), batchSize) + err := ng.InstanceStore.FullSync(ctx, append(instances, generateTestAlertInstance(orgID, newRuleUID)), batchSize) require.NoError(t, err) - res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ + res, err := ng.InstanceStore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ RuleOrgID: orgID, }) require.NoError(t, err) @@ -405,16 +523,16 @@ func TestIntegrationFullSync(t *testing.T) { generateTestAlertInstance(orgID, "preexisting-1"), generateTestAlertInstance(orgID, "preexisting-2"), } - err := dbstore.FullSync(ctx, initialInstances, 5) + err := ng.InstanceStore.FullSync(ctx, initialInstances, 5) require.NoError(t, err) // Now call FullSync with no instances. According to the code, this should return nil // and should not delete anything in the table. - err = dbstore.FullSync(ctx, []models.AlertInstance{}, 5) + err = ng.InstanceStore.FullSync(ctx, []models.AlertInstance{}, 5) require.NoError(t, err) // Check that the previously inserted instances are still present. - res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ + res, err := ng.InstanceStore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ RuleOrgID: orgID, }) require.NoError(t, err) @@ -441,11 +559,11 @@ func TestIntegrationFullSync(t *testing.T) { // Make the invalid instance actually invalid invalidInstance.AlertInstanceKey.RuleUID = "" - err := dbstore.FullSync(ctx, []models.AlertInstance{validInstance, invalidInstance}, 2) + err := ng.InstanceStore.FullSync(ctx, []models.AlertInstance{validInstance, invalidInstance}, 2) require.NoError(t, err) // Only the valid instance should be saved. - res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ + res, err := ng.InstanceStore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ RuleOrgID: orgID, }) require.NoError(t, err) @@ -460,10 +578,10 @@ func TestIntegrationFullSync(t *testing.T) { generateTestAlertInstance(orgID, "batch-test2"), } - err := dbstore.FullSync(ctx, smallSet, 100) + err := ng.InstanceStore.FullSync(ctx, smallSet, 100) require.NoError(t, err) - res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ + res, err := ng.InstanceStore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ RuleOrgID: orgID, }) require.NoError(t, err) @@ -483,7 +601,7 @@ func TestIntegrationFullSync(t *testing.T) { t.Run("Should handle a large set of instances with a moderate batchSize", func(t *testing.T) { // Clear everything first. - err := dbstore.FullSync(ctx, []models.AlertInstance{}, 1) + err := ng.InstanceStore.FullSync(ctx, []models.AlertInstance{}, 1) require.NoError(t, err) largeCount := 300 @@ -492,10 +610,10 @@ func TestIntegrationFullSync(t *testing.T) { largeSet[i] = generateTestAlertInstance(orgID, fmt.Sprintf("large-%d", i)) } - err = dbstore.FullSync(ctx, largeSet, 50) + err = ng.InstanceStore.FullSync(ctx, largeSet, 50) require.NoError(t, err) - res, err := dbstore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ + res, err := ng.InstanceStore.ListAlertInstances(ctx, &models.ListAlertInstancesQuery{ RuleOrgID: orgID, }) require.NoError(t, err) @@ -503,6 +621,77 @@ func TestIntegrationFullSync(t *testing.T) { }) } +func TestIntegration_ProtoInstanceDBStore_VerifyCompressedData(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + ng, dbstore := tests.SetupTestEnv( + t, + baseIntervalSeconds, + tests.WithFeatureToggles( + featuremgmt.WithFeatures( + featuremgmt.FlagAlertingSaveStateCompressed, + ), + ), + ) + + alertRule := tests.CreateTestAlertRule(t, ctx, dbstore, 60, 1) + + labelsHash := "hash1" + reason := "reason" + state := models.InstanceStateFiring + instances := []models.AlertInstance{ + createAlertInstance(alertRule.OrgID, alertRule.UID, labelsHash, reason, state), + } + + err := ng.InstanceStore.SaveAlertInstancesForRule(ctx, alertRule.GetKeyWithGroup(), instances) + require.NoError(t, err) + + // Query raw data from the database + type compressedRow struct { + OrgID int64 `xorm:"org_id"` + RuleUID string `xorm:"rule_uid"` + Data []byte `xorm:"data"` + } + var rawData compressedRow + err = dbstore.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { + _, err := sess.SQL("SELECT * FROM alert_rule_state").Get(&rawData) + return err + }) + require.NoError(t, err) + + // Decompress and compare + require.NotNil(t, rawData) + decompressedInstances, err := decompressAlertInstances(rawData.Data) + require.NoError(t, err) + + require.Len(t, decompressedInstances, 1) + require.Equal(t, instances[0].LabelsHash, decompressedInstances[0].LabelsHash) + require.Equal(t, string(instances[0].CurrentState), decompressedInstances[0].CurrentState) + require.Equal(t, instances[0].CurrentReason, decompressedInstances[0].CurrentReason) +} + +func decompressAlertInstances(compressed []byte) ([]*pb.AlertInstance, error) { + if len(compressed) == 0 { + return nil, nil + } + + reader := snappy.NewReader(bytes.NewReader(compressed)) + var b bytes.Buffer + if _, err := b.ReadFrom(reader); err != nil { + return nil, fmt.Errorf("failed to read compressed data: %w", err) + } + + var instances pb.AlertInstances + if err := proto.Unmarshal(b.Bytes(), &instances); err != nil { + return nil, fmt.Errorf("failed to unmarshal protobuf: %w", err) + } + + return instances.Instances, nil +} + func generateTestAlertInstance(orgID int64, ruleID string) models.AlertInstance { return models.AlertInstance{ AlertInstanceKey: models.AlertInstanceKey{ diff --git a/pkg/services/ngalert/store/proto/v1/alert_rule_state.pb.go b/pkg/services/ngalert/store/proto/v1/alert_rule_state.pb.go new file mode 100644 index 00000000000..e07a4bbdf04 --- /dev/null +++ b/pkg/services/ngalert/store/proto/v1/alert_rule_state.pb.go @@ -0,0 +1,301 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.1 +// protoc (unknown) +// source: alert_rule_state.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AlertInstance struct { + state protoimpl.MessageState `protogen:"open.v1"` + LabelsHash string `protobuf:"bytes,1,opt,name=labels_hash,json=labelsHash,proto3" json:"labels_hash,omitempty"` + Labels map[string]string `protobuf:"bytes,2,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + CurrentState string `protobuf:"bytes,3,opt,name=current_state,json=currentState,proto3" json:"current_state,omitempty"` + CurrentReason string `protobuf:"bytes,4,opt,name=current_reason,json=currentReason,proto3" json:"current_reason,omitempty"` + CurrentStateSince *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=current_state_since,json=currentStateSince,proto3" json:"current_state_since,omitempty"` + CurrentStateEnd *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=current_state_end,json=currentStateEnd,proto3" json:"current_state_end,omitempty"` + LastEvalTime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=last_eval_time,json=lastEvalTime,proto3" json:"last_eval_time,omitempty"` + LastSentAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=last_sent_at,json=lastSentAt,proto3" json:"last_sent_at,omitempty"` + ResolvedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=resolved_at,json=resolvedAt,proto3" json:"resolved_at,omitempty"` + ResultFingerprint string `protobuf:"bytes,10,opt,name=result_fingerprint,json=resultFingerprint,proto3" json:"result_fingerprint,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AlertInstance) Reset() { + *x = AlertInstance{} + mi := &file_alert_rule_state_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AlertInstance) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AlertInstance) ProtoMessage() {} + +func (x *AlertInstance) ProtoReflect() protoreflect.Message { + mi := &file_alert_rule_state_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AlertInstance.ProtoReflect.Descriptor instead. +func (*AlertInstance) Descriptor() ([]byte, []int) { + return file_alert_rule_state_proto_rawDescGZIP(), []int{0} +} + +func (x *AlertInstance) GetLabelsHash() string { + if x != nil { + return x.LabelsHash + } + return "" +} + +func (x *AlertInstance) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +func (x *AlertInstance) GetCurrentState() string { + if x != nil { + return x.CurrentState + } + return "" +} + +func (x *AlertInstance) GetCurrentReason() string { + if x != nil { + return x.CurrentReason + } + return "" +} + +func (x *AlertInstance) GetCurrentStateSince() *timestamppb.Timestamp { + if x != nil { + return x.CurrentStateSince + } + return nil +} + +func (x *AlertInstance) GetCurrentStateEnd() *timestamppb.Timestamp { + if x != nil { + return x.CurrentStateEnd + } + return nil +} + +func (x *AlertInstance) GetLastEvalTime() *timestamppb.Timestamp { + if x != nil { + return x.LastEvalTime + } + return nil +} + +func (x *AlertInstance) GetLastSentAt() *timestamppb.Timestamp { + if x != nil { + return x.LastSentAt + } + return nil +} + +func (x *AlertInstance) GetResolvedAt() *timestamppb.Timestamp { + if x != nil { + return x.ResolvedAt + } + return nil +} + +func (x *AlertInstance) GetResultFingerprint() string { + if x != nil { + return x.ResultFingerprint + } + return "" +} + +type AlertInstances struct { + state protoimpl.MessageState `protogen:"open.v1"` + Instances []*AlertInstance `protobuf:"bytes,1,rep,name=instances,proto3" json:"instances,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AlertInstances) Reset() { + *x = AlertInstances{} + mi := &file_alert_rule_state_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AlertInstances) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AlertInstances) ProtoMessage() {} + +func (x *AlertInstances) ProtoReflect() protoreflect.Message { + mi := &file_alert_rule_state_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AlertInstances.ProtoReflect.Descriptor instead. +func (*AlertInstances) Descriptor() ([]byte, []int) { + return file_alert_rule_state_proto_rawDescGZIP(), []int{1} +} + +func (x *AlertInstances) GetInstances() []*AlertInstance { + if x != nil { + return x.Instances + } + return nil +} + +var File_alert_rule_state_proto protoreflect.FileDescriptor + +var file_alert_rule_state_proto_rawDesc = []byte{ + 0x0a, 0x16, 0x61, 0x6c, 0x65, 0x72, 0x74, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x6e, 0x67, 0x61, 0x6c, 0x65, 0x72, + 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xfc, 0x04, 0x0a, 0x0d, + 0x41, 0x6c, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1f, 0x0a, + 0x0b, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x48, 0x61, 0x73, 0x68, 0x12, 0x43, + 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, + 0x2e, 0x6e, 0x67, 0x61, 0x6c, 0x65, 0x72, 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x2e, + 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, + 0x65, 0x6c, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x75, 0x72, 0x72, + 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x75, 0x72, 0x72, + 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, + 0x4a, 0x0a, 0x13, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x5f, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x11, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, + 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x46, 0x0a, 0x11, 0x63, + 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x65, 0x6e, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x0f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x45, 0x6e, 0x64, 0x12, 0x40, 0x0a, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x65, 0x76, 0x61, 0x6c, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x76, 0x61, + 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x3c, 0x0a, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, + 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x6e, + 0x74, 0x41, 0x74, 0x12, 0x3b, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x41, 0x74, + 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, + 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x1a, + 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x4f, 0x0a, 0x0e, 0x41, 0x6c, + 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x3d, 0x0a, 0x09, + 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1f, 0x2e, 0x6e, 0x67, 0x61, 0x6c, 0x65, 0x72, 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x52, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x42, 0x40, 0x5a, 0x3e, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, + 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x6e, 0x67, 0x61, 0x6c, 0x65, 0x72, 0x74, 0x2f, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_alert_rule_state_proto_rawDescOnce sync.Once + file_alert_rule_state_proto_rawDescData = file_alert_rule_state_proto_rawDesc +) + +func file_alert_rule_state_proto_rawDescGZIP() []byte { + file_alert_rule_state_proto_rawDescOnce.Do(func() { + file_alert_rule_state_proto_rawDescData = protoimpl.X.CompressGZIP(file_alert_rule_state_proto_rawDescData) + }) + return file_alert_rule_state_proto_rawDescData +} + +var file_alert_rule_state_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_alert_rule_state_proto_goTypes = []any{ + (*AlertInstance)(nil), // 0: ngalert.store.v1.AlertInstance + (*AlertInstances)(nil), // 1: ngalert.store.v1.AlertInstances + nil, // 2: ngalert.store.v1.AlertInstance.LabelsEntry + (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp +} +var file_alert_rule_state_proto_depIdxs = []int32{ + 2, // 0: ngalert.store.v1.AlertInstance.labels:type_name -> ngalert.store.v1.AlertInstance.LabelsEntry + 3, // 1: ngalert.store.v1.AlertInstance.current_state_since:type_name -> google.protobuf.Timestamp + 3, // 2: ngalert.store.v1.AlertInstance.current_state_end:type_name -> google.protobuf.Timestamp + 3, // 3: ngalert.store.v1.AlertInstance.last_eval_time:type_name -> google.protobuf.Timestamp + 3, // 4: ngalert.store.v1.AlertInstance.last_sent_at:type_name -> google.protobuf.Timestamp + 3, // 5: ngalert.store.v1.AlertInstance.resolved_at:type_name -> google.protobuf.Timestamp + 0, // 6: ngalert.store.v1.AlertInstances.instances:type_name -> ngalert.store.v1.AlertInstance + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_alert_rule_state_proto_init() } +func file_alert_rule_state_proto_init() { + if File_alert_rule_state_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_alert_rule_state_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_alert_rule_state_proto_goTypes, + DependencyIndexes: file_alert_rule_state_proto_depIdxs, + MessageInfos: file_alert_rule_state_proto_msgTypes, + }.Build() + File_alert_rule_state_proto = out.File + file_alert_rule_state_proto_rawDesc = nil + file_alert_rule_state_proto_goTypes = nil + file_alert_rule_state_proto_depIdxs = nil +} diff --git a/pkg/services/ngalert/store/proto/v1/alert_rule_state.proto b/pkg/services/ngalert/store/proto/v1/alert_rule_state.proto new file mode 100644 index 00000000000..3f0ec7de689 --- /dev/null +++ b/pkg/services/ngalert/store/proto/v1/alert_rule_state.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package ngalert.store.v1; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/grafana/grafana/pkg/services/ngalert/store/proto/v1"; + +message AlertInstance { + string labels_hash = 1; + map labels = 2; + string current_state = 3; + string current_reason = 4; + google.protobuf.Timestamp current_state_since = 5; + google.protobuf.Timestamp current_state_end = 6; + google.protobuf.Timestamp last_eval_time = 7; + google.protobuf.Timestamp last_sent_at = 8; + google.protobuf.Timestamp resolved_at = 9; + string result_fingerprint = 10; +} + +message AlertInstances { + repeated AlertInstance instances = 1; +} diff --git a/pkg/services/ngalert/store/proto/v1/buf.gen.yaml b/pkg/services/ngalert/store/proto/v1/buf.gen.yaml new file mode 100644 index 00000000000..552d42bc105 --- /dev/null +++ b/pkg/services/ngalert/store/proto/v1/buf.gen.yaml @@ -0,0 +1,5 @@ +version: v1 +plugins: + - plugin: go + out: pkg/services/ngalert/store/proto/v1 + opt: paths=source_relative diff --git a/pkg/services/ngalert/store/proto/v1/buf.yaml b/pkg/services/ngalert/store/proto/v1/buf.yaml new file mode 100644 index 00000000000..4cb6a23ed85 --- /dev/null +++ b/pkg/services/ngalert/store/proto/v1/buf.yaml @@ -0,0 +1,7 @@ +version: v2 +lint: + use: + - DEFAULT +breaking: + use: + - FILE diff --git a/pkg/services/ngalert/store/proto_instance_database.go b/pkg/services/ngalert/store/proto_instance_database.go new file mode 100644 index 00000000000..e713d43be31 --- /dev/null +++ b/pkg/services/ngalert/store/proto_instance_database.go @@ -0,0 +1,261 @@ +package store + +import ( + "bytes" + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/golang/snappy" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/ngalert/models" + pb "github.com/grafana/grafana/pkg/services/ngalert/store/proto/v1" +) + +// ProtoInstanceDBStore is a store for alert instances that stores state of a rule as a single +// row in the database with alert instances as a compressed protobuf message. +type ProtoInstanceDBStore struct { + SQLStore db.DB + Logger log.Logger + FeatureToggles featuremgmt.FeatureToggles +} + +func (st ProtoInstanceDBStore) ListAlertInstances(ctx context.Context, cmd *models.ListAlertInstancesQuery) (result []*models.AlertInstance, err error) { + logger := st.Logger.FromContext(ctx) + logger.Debug("ListAlertInstances called", "rule_uid", cmd.RuleUID, "org_id", cmd.RuleOrgID) + alertInstances := make([]*models.AlertInstance, 0) + + err = st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { + s := strings.Builder{} + params := make([]any, 0) + + addToQuery := func(stmt string, p ...any) { + s.WriteString(stmt) + params = append(params, p...) + } + + addToQuery("SELECT * FROM alert_rule_state WHERE org_id = ?", cmd.RuleOrgID) + + if cmd.RuleUID != "" { + addToQuery(" AND rule_uid = ?", cmd.RuleUID) + } + + // Execute query to get compressed instances + type compressedRow struct { + OrgID int64 `xorm:"org_id"` + RuleUID string `xorm:"rule_uid"` + Data []byte `xorm:"data"` + } + + rows := make([]compressedRow, 0) + if err := sess.SQL(s.String(), params...).Find(&rows); err != nil { + return fmt.Errorf("failed to query alert_rule_state: %w", err) + } + + for _, row := range rows { + instances, err := decompressAlertInstances(row.Data) + if err != nil { + return fmt.Errorf("failed to decompress alert instances for rule %s: %w", row.RuleUID, err) + } + + // Convert proto instances to model instances + for _, protoInstance := range instances { + modelInstance := alertInstanceProtoToModel(row.RuleUID, row.OrgID, protoInstance) + if modelInstance != nil { + // If FlagAlertingNoNormalState is enabled, we should not return instances with normal state and no reason. + if st.FeatureToggles.IsEnabled(ctx, featuremgmt.FlagAlertingNoNormalState) { + if modelInstance.CurrentState == models.InstanceStateNormal && modelInstance.CurrentReason == "" { + continue + } + } + alertInstances = append(alertInstances, modelInstance) + } + } + } + + return nil + }) + + logger.Debug("ListAlertInstances completed", "instances", len(alertInstances)) + + return alertInstances, err +} + +func (st ProtoInstanceDBStore) SaveAlertInstance(ctx context.Context, alertInstance models.AlertInstance) error { + st.Logger.Error("SaveAlertInstance called and not implemented") + return errors.New("save alert instance is not implemented for proto instance database store") +} + +func (st ProtoInstanceDBStore) FetchOrgIds(ctx context.Context) ([]int64, error) { + orgIds := []int64{} + + err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { + s := strings.Builder{} + params := make([]any, 0) + + addToQuery := func(stmt string, p ...any) { + s.WriteString(stmt) + params = append(params, p...) + } + + addToQuery("SELECT DISTINCT org_id FROM alert_rule_state") + + if err := sess.SQL(s.String(), params...).Find(&orgIds); err != nil { + return err + } + return nil + }) + + return orgIds, err +} + +func (st ProtoInstanceDBStore) DeleteAlertInstances(ctx context.Context, keys ...models.AlertInstanceKey) error { + logger := st.Logger.FromContext(ctx) + logger.Error("DeleteAlertInstances called and not implemented") + return errors.New("delete alert instances is not implemented for proto instance database store") +} + +func (st ProtoInstanceDBStore) SaveAlertInstancesForRule(ctx context.Context, key models.AlertRuleKeyWithGroup, instances []models.AlertInstance) error { + logger := st.Logger.FromContext(ctx) + logger.Debug("SaveAlertInstancesForRule called", "rule_uid", key.UID, "org_id", key.OrgID, "instances", len(instances)) + + alert_instances_proto := make([]*pb.AlertInstance, len(instances)) + + for i, instance := range instances { + alert_instances_proto[i] = alertInstanceModelToProto(instance) + } + + compressedAlertInstances, err := compressAlertInstances(alert_instances_proto) + if err != nil { + return fmt.Errorf("failed to compress alert instances: %w", err) + } + + return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + params := []any{key.OrgID, key.UID, compressedAlertInstances, time.Now()} + + upsertSQL := st.SQLStore.GetDialect().UpsertSQL( + "alert_rule_state", + []string{"org_id", "rule_uid"}, + []string{"org_id", "rule_uid", "data", "updated_at"}, + ) + _, err = sess.SQL(upsertSQL, params...).Query() + + return err + }) +} + +func (st ProtoInstanceDBStore) DeleteAlertInstancesByRule(ctx context.Context, key models.AlertRuleKeyWithGroup) error { + logger := st.Logger.FromContext(ctx) + logger.Debug("DeleteAlertInstancesByRule called", "rule_uid", key.UID, "org_id", key.OrgID) + + return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + _, err := sess.Exec("DELETE FROM alert_rule_state WHERE org_id = ? AND rule_uid = ?", key.OrgID, key.UID) + return err + }) +} + +func (st ProtoInstanceDBStore) FullSync(ctx context.Context, instances []models.AlertInstance, batchSize int) error { + logger := st.Logger.FromContext(ctx) + logger.Error("FullSync called and not implemented") + return errors.New("fullsync is not implemented for proto instance database store") +} + +func alertInstanceModelToProto(modelInstance models.AlertInstance) *pb.AlertInstance { + return &pb.AlertInstance{ + Labels: modelInstance.Labels, + LabelsHash: modelInstance.LabelsHash, + CurrentState: string(modelInstance.CurrentState), + CurrentStateSince: timestamppb.New(modelInstance.CurrentStateSince), + CurrentStateEnd: timestamppb.New(modelInstance.CurrentStateEnd), + CurrentReason: modelInstance.CurrentReason, + LastEvalTime: timestamppb.New(modelInstance.LastEvalTime), + LastSentAt: nullableTimeToTimestamp(modelInstance.LastSentAt), + ResolvedAt: nullableTimeToTimestamp(modelInstance.ResolvedAt), + ResultFingerprint: modelInstance.ResultFingerprint, + } +} + +func compressAlertInstances(instances []*pb.AlertInstance) ([]byte, error) { + mProto, err := proto.Marshal(&pb.AlertInstances{Instances: instances}) + if err != nil { + return nil, fmt.Errorf("failed to marshal protobuf: %w", err) + } + + var b bytes.Buffer + writer := snappy.NewBufferedWriter(&b) + if _, err := writer.Write(mProto); err != nil { + return nil, fmt.Errorf("failed to write compressed data: %w", err) + } + + if err := writer.Close(); err != nil { + return nil, fmt.Errorf("failed to close snappy writer: %w", err) + } + + return b.Bytes(), nil +} + +func alertInstanceProtoToModel(ruleUID string, ruleOrgID int64, protoInstance *pb.AlertInstance) *models.AlertInstance { + if protoInstance == nil { + return nil + } + + return &models.AlertInstance{ + AlertInstanceKey: models.AlertInstanceKey{ + RuleOrgID: ruleOrgID, + RuleUID: ruleUID, + LabelsHash: protoInstance.LabelsHash, + }, + Labels: protoInstance.Labels, + CurrentState: models.InstanceStateType(protoInstance.CurrentState), + CurrentStateSince: protoInstance.CurrentStateSince.AsTime(), + CurrentStateEnd: protoInstance.CurrentStateEnd.AsTime(), + CurrentReason: protoInstance.CurrentReason, + LastEvalTime: protoInstance.LastEvalTime.AsTime(), + LastSentAt: nullableTimestampToTime(protoInstance.LastSentAt), + ResolvedAt: nullableTimestampToTime(protoInstance.ResolvedAt), + ResultFingerprint: protoInstance.ResultFingerprint, + } +} + +func decompressAlertInstances(compressed []byte) ([]*pb.AlertInstance, error) { + if len(compressed) == 0 { + return nil, nil + } + + reader := snappy.NewReader(bytes.NewReader(compressed)) + var b bytes.Buffer + if _, err := b.ReadFrom(reader); err != nil { + return nil, fmt.Errorf("failed to read compressed data: %w", err) + } + + var instances pb.AlertInstances + if err := proto.Unmarshal(b.Bytes(), &instances); err != nil { + return nil, fmt.Errorf("failed to unmarshal protobuf: %w", err) + } + + return instances.Instances, nil +} + +// nullableTimeToTimestamp converts a nullable time.Time to nil, if it is nil, otherwise it converts to timestamppb.Timestamp. +func nullableTimeToTimestamp(t *time.Time) *timestamppb.Timestamp { + if t == nil { + return nil + } + return timestamppb.New(*t) +} + +// nullableTimestampToTime converts a nullable timestamppb.Timestamp to nil, if it is nil, otherwise it converts to time.Time. +func nullableTimestampToTime(ts *timestamppb.Timestamp) *time.Time { + if ts == nil { + return nil + } + t := ts.AsTime() + return &t +} diff --git a/pkg/services/ngalert/store/proto_instance_database_test.go b/pkg/services/ngalert/store/proto_instance_database_test.go new file mode 100644 index 00000000000..0993a68da72 --- /dev/null +++ b/pkg/services/ngalert/store/proto_instance_database_test.go @@ -0,0 +1,176 @@ +package store + +import ( + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/grafana/grafana/pkg/services/ngalert/models" + pb "github.com/grafana/grafana/pkg/services/ngalert/store/proto/v1" +) + +func TestAlertInstanceModelToProto(t *testing.T) { + currentStateSince := time.Now() + currentStateEnd := currentStateSince.Add(time.Minute) + lastEvalTime := currentStateSince.Add(-time.Minute) + lastSentAt := currentStateSince.Add(-2 * time.Minute) + resolvedAt := currentStateSince.Add(-3 * time.Minute) + + tests := []struct { + name string + input models.AlertInstance + expected *pb.AlertInstance + }{ + { + name: "valid instance", + input: models.AlertInstance{ + Labels: map[string]string{"key": "value"}, + AlertInstanceKey: models.AlertInstanceKey{ + RuleUID: "rule-uid-1", + RuleOrgID: 1, + LabelsHash: "hash123", + }, + CurrentState: models.InstanceStateFiring, + CurrentStateSince: currentStateSince, + CurrentStateEnd: currentStateEnd, + CurrentReason: "Some reason", + LastEvalTime: lastEvalTime, + LastSentAt: &lastSentAt, + ResolvedAt: &resolvedAt, + ResultFingerprint: "fingerprint", + }, + expected: &pb.AlertInstance{ + Labels: map[string]string{"key": "value"}, + LabelsHash: "hash123", + CurrentState: "Alerting", + CurrentStateSince: timestamppb.New(currentStateSince), + CurrentStateEnd: timestamppb.New(currentStateEnd), + CurrentReason: "Some reason", + LastEvalTime: timestamppb.New(lastEvalTime), + LastSentAt: toProtoTimestampPtr(&lastSentAt), + ResolvedAt: toProtoTimestampPtr(&resolvedAt), + ResultFingerprint: "fingerprint", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := alertInstanceModelToProto(tt.input) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestAlertInstanceProtoToModel(t *testing.T) { + currentStateSince := time.Now().UTC() + currentStateEnd := currentStateSince.Add(time.Minute).UTC() + lastEvalTime := currentStateSince.Add(-time.Minute).UTC() + lastSentAt := currentStateSince.Add(-2 * time.Minute).UTC() + resolvedAt := currentStateSince.Add(-3 * time.Minute).UTC() + ruleUID := "rule-uid-1" + orgID := int64(1) + + tests := []struct { + name string + input *pb.AlertInstance + expected *models.AlertInstance + }{ + { + name: "valid instance", + input: &pb.AlertInstance{ + Labels: map[string]string{"key": "value"}, + LabelsHash: "hash123", + CurrentState: "Alerting", + CurrentStateSince: timestamppb.New(currentStateSince), + CurrentStateEnd: timestamppb.New(currentStateEnd), + LastEvalTime: timestamppb.New(lastEvalTime), + LastSentAt: toProtoTimestampPtr(&lastSentAt), + ResolvedAt: toProtoTimestampPtr(&resolvedAt), + ResultFingerprint: "fingerprint", + }, + expected: &models.AlertInstance{ + Labels: map[string]string{"key": "value"}, + AlertInstanceKey: models.AlertInstanceKey{ + RuleUID: ruleUID, + RuleOrgID: orgID, + LabelsHash: "hash123", + }, + CurrentState: models.InstanceStateFiring, + CurrentStateSince: currentStateSince, + CurrentStateEnd: currentStateEnd, + LastEvalTime: lastEvalTime, + LastSentAt: &lastSentAt, + ResolvedAt: &resolvedAt, + ResultFingerprint: "fingerprint", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := alertInstanceProtoToModel(ruleUID, orgID, tt.input) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestModelAlertInstanceMatchesProtobuf(t *testing.T) { + // The AlertInstance protobuf must always contain the same information + // as the model, so that it's preserved between the Grafana restarts. + // + // If the AlertInstance model changes, review the protobuf and the test + // and update them accordingly. + t.Run("when AlertInstance model changes", func(t *testing.T) { + modelType := reflect.TypeOf(models.AlertInstance{}) + require.Equal(t, 10, modelType.NumField(), "AlertInstance model has changed, update the protobuf") + }) +} + +func TestCompressAndDecompressAlertInstances(t *testing.T) { + now := time.Now() + + alertInstances := []*pb.AlertInstance{ + { + Labels: map[string]string{"label-1": "value-1"}, + LabelsHash: "hash-1", + CurrentState: "normal", + CurrentStateSince: timestamppb.New(now), + CurrentStateEnd: timestamppb.New(now.Add(time.Hour)), + CurrentReason: "reason-1", + LastEvalTime: timestamppb.New(now.Add(-time.Minute)), + ResolvedAt: timestamppb.New(now.Add(time.Hour * 2)), + ResultFingerprint: "fingerprint-1", + }, + { + Labels: map[string]string{"label-2": "value-2"}, + LabelsHash: "hash-2", + CurrentState: "firing", + CurrentStateSince: timestamppb.New(now), + CurrentReason: "reason-2", + LastEvalTime: timestamppb.New(now.Add(-time.Minute * 2)), + }, + } + + compressedData, err := compressAlertInstances(alertInstances) + require.NoError(t, err) + + decompressedInstances, err := decompressAlertInstances(compressedData) + require.NoError(t, err) + + // Compare the original and decompressed instances + require.Equal(t, len(alertInstances), len(decompressedInstances)) + require.EqualExportedValues(t, alertInstances[0], decompressedInstances[0]) + require.EqualExportedValues(t, alertInstances[1], decompressedInstances[1]) +} + +func toProtoTimestampPtr(tm *time.Time) *timestamppb.Timestamp { + if tm == nil { + return nil + } + + return timestamppb.New(*tm) +} diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 6188d6291a9..0d356fabd7b 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -37,14 +37,35 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/database" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) +type TestEnvOptions struct { + featureToggles featuremgmt.FeatureToggles +} + +type TestEnvOption func(*TestEnvOptions) + +func WithFeatureToggles(toggles featuremgmt.FeatureToggles) TestEnvOption { + return func(opts *TestEnvOptions) { + opts.featureToggles = toggles + } +} + // SetupTestEnv initializes a store to used by the tests. -func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG, *store.DBstore) { +func SetupTestEnv(tb testing.TB, baseInterval time.Duration, opts ...TestEnvOption) (*ngalert.AlertNG, *store.DBstore) { tb.Helper() + options := TestEnvOptions{ + featureToggles: featuremgmt.WithFeatures(), + } + + for _, opt := range opts { + opt(&options) + } + cfg := setting.NewCfg() cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{ BaseInterval: setting.SchedulerBaseInterval, @@ -64,18 +85,18 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG, bus := bus.ProvideBus(tracer) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) dashboardService, dashboardStore := testutil.SetupDashboardService(tb, sqlStore, folderStore, cfg) - features := featuremgmt.WithFeatures() - folderService := testutil.SetupFolderService(tb, cfg, sqlStore, dashboardStore, folderStore, bus, features, ac) - ruleStore, err := store.ProvideDBStore(cfg, featuremgmt.WithFeatures(), sqlStore, folderService, &dashboards.FakeDashboardService{}, ac, bus) + folderService := testutil.SetupFolderService(tb, cfg, sqlStore, dashboardStore, folderStore, bus, options.featureToggles, ac) + ruleStore, err := store.ProvideDBStore(cfg, options.featureToggles, sqlStore, folderService, &dashboards.FakeDashboardService{}, ac, bus) require.NoError(tb, err) ng, err := ngalert.ProvideService( - cfg, features, nil, nil, routing.NewRouteRegister(), sqlStore, kvstore.NewFakeKVStore(), nil, nil, quotatest.New(false, nil), + cfg, options.featureToggles, nil, nil, routing.NewRouteRegister(), sqlStore, kvstore.NewFakeKVStore(), nil, nil, quotatest.New(false, nil), secretsService, nil, m, folderService, ac, &dashboards.FakeDashboardService{}, nil, bus, ac, - annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), + annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), usertest.NewUserServiceFake(), ) require.NoError(tb, err) + return ng, &store.DBstore{ - FeatureToggles: features, + FeatureToggles: options.featureToggles, SQLStore: ng.SQLStore, Cfg: setting.UnifiedAlertingSettings{ BaseInterval: baseInterval * time.Second, diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index 4030fe0c19b..5cf4a3acabf 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -50,6 +50,7 @@ import ( "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -512,7 +513,7 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe _, err = ngalert.ProvideService( cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, ngalertfakes.NewFakeKVStore(t), nil, nil, quotaService, secretsService, nil, m, &foldertest.FakeService{}, &acmock.Mock{}, &dashboards.FakeDashboardService{}, nil, b, &acmock.Mock{}, - annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), + annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(), usertest.NewUserServiceFake(), ) require.NoError(t, err) _, err = storesrv.ProvideService(sqlStore, featuremgmt.WithFeatures(), cfg, quotaService, storesrv.ProvideSystemUsersService()) diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 317eb39a13d..40704dac838 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -143,4 +143,6 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) { accesscontrol.AddReceiverCreateScopeMigration(mg) ualert.AddAlertRuleUpdatedByMigration(mg) + + ualert.AddAlertRuleStateTable(mg) } diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule_state_table.go b/pkg/services/sqlstore/migrations/ualert/alert_rule_state_table.go new file mode 100644 index 00000000000..bf2a662c063 --- /dev/null +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule_state_table.go @@ -0,0 +1,29 @@ +package ualert + +import "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +// AddAlertRuleStateTable adds column to store alert rule state data. +func AddAlertRuleStateTable(mg *migrator.Migrator) { + alertStateTable := migrator.Table{ + Name: "alert_rule_state", + Columns: []*migrator.Column{ + {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "rule_uid", Type: migrator.DB_NVarchar, Length: UIDMaxLength, Nullable: false}, + {Name: "data", Type: migrator.DB_LongBlob, Nullable: false}, + {Name: "updated_at", Type: migrator.DB_DateTime, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"org_id", "rule_uid"}, Type: migrator.UniqueIndex}, + }, + } + + mg.AddMigration( + "add alert_rule_state table", + migrator.NewAddTableMigration(alertStateTable), + ) + mg.AddMigration( + "add index to alert_rule_state on org_id and rule_uid columns", + migrator.NewAddIndexMigration(alertStateTable, alertStateTable.Indices[0]), + ) +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 42a123a6b8b..b53b34e05f3 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -541,6 +541,8 @@ type Cfg struct { HttpsSkipVerify bool } +const UnifiedStorageConfigKeyDashboard = "dashboards.dashboard.grafana.app" + type UnifiedStorageConfig struct { DualWriterMode rest.DualWriterMode DualWriterPeriodicDataSyncJobEnabled bool diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 9126d4b0d3f..6f3f676f346 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -17,6 +17,7 @@ require ( github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible github.com/grafana/grafana-plugin-sdk-go v0.263.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d + github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/prometheus/client_golang v1.20.5 @@ -121,7 +122,6 @@ require ( github.com/grafana/grafana-app-sdk/logging v0.29.0 // indirect github.com/grafana/grafana-aws-sdk v0.31.5 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect - github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/grafana/sqlds/v4 v4.1.3 // indirect diff --git a/pkg/storage/unified/resource/search_client.go b/pkg/storage/unified/resource/search_client.go new file mode 100644 index 00000000000..43e0faa7b1e --- /dev/null +++ b/pkg/storage/unified/resource/search_client.go @@ -0,0 +1,20 @@ +package resource + +import ( + "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/setting" +) + +func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedClient ResourceIndexClient, legacyClient ResourceIndexClient) ResourceIndexClient { + config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey] + if !ok { + return legacyClient + } + + switch config.DualWriterMode { + case rest.Mode0, rest.Mode1, rest.Mode2: + return legacyClient + default: + return unifiedClient + } +} diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 18859d984ad..79a5149efc7 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -575,7 +575,7 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resource.Res if req.Query != "" && req.Query != "*" { searchrequest.Fields = append(searchrequest.Fields, resource.SEARCH_FIELD_SCORE) // mimic the behavior of the sql search - query := req.Query + query := strings.ToLower(req.Query) if !strings.Contains(query, "*") { query = "*" + query + "*" } diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index a2c151a53eb..ec9cda827b8 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -877,4 +877,136 @@ func TestIntegrationExportFileProvision(t *testing.T) { require.YAMLEq(t, string(expectedYaml), exportRaw) }) }) + t.Run("when provisioning mute times from files", func(t *testing.T) { + // add file provisioned mute times + fileProvisionedMuteTimings, err := testData.ReadFile(path.Join("test-data", "provisioning-mute-times.yaml")) + require.NoError(t, err) + + var expected definitions.AlertingFileExport + require.NoError(t, yaml.Unmarshal(fileProvisionedMuteTimings, &expected)) + expected.MuteTimings[0].OrgID = 1 // HACK to deal with weird goyaml behavior + expectedYamlRaw, err := yaml.Marshal(expected) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(alertingDir, "provisioning-mute-times.yaml"), fileProvisionedMuteTimings, 0750) + require.NoError(t, err) + + apiClient.ReloadAlertingFileProvisioning(t) + + t.Run("exported mute times shouldn't escape $ characters", func(t *testing.T) { + // call export endpoint + exportRaw := apiClient.ExportMuteTiming(t, "$mute_time_a", "yaml") + var export definitions.AlertingFileExport + require.NoError(t, yaml.Unmarshal([]byte(exportRaw), &export)) + expectedYaml := string(expectedYamlRaw) + // verify the file exported matches the file provisioned thing + require.Len(t, export.MuteTimings, 1) + require.YAMLEq(t, expectedYaml, exportRaw) + }) + }) +} + +func TestIntegrationExportFileProvisionMixed(t *testing.T) { + dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + }) + + provisioningDir := filepath.Join(dir, "conf", "provisioning") + alertingDir := filepath.Join(provisioningDir, "alerting") + err := os.MkdirAll(alertingDir, 0750) + require.NoError(t, err) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) + + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin") + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "admin", + Login: "admin", + IsAdmin: true, + }) + + apiClient.ReloadCachedPermissions(t) + t.Run("when provisioning mixed set of alerting configurations from files", func(t *testing.T) { + // add file provisioned mixed set of alerting configurations + fileProvisionedResources, err := testData.ReadFile(path.Join("test-data", "provisioning-mixed-set.yaml")) + require.NoError(t, err) + + var expected definitions.AlertingFileExport + require.NoError(t, yaml.Unmarshal(fileProvisionedResources, &expected)) + expected.MuteTimings[0].OrgID = 1 // HACK to deal with weird goyaml behavior + + err = os.WriteFile(filepath.Join(alertingDir, "provisioning-mixed-set.yaml"), fileProvisionedResources, 0750) + require.NoError(t, err) + + apiClient.ReloadAlertingFileProvisioning(t) + + t.Run("exported notification policy matches imported", func(t *testing.T) { + notificationPolicyExpected := expected + notificationPolicyExpected.MuteTimings = nil + notificationPolicyExpected.ContactPoints = nil + notificationPolicyExpected.Groups = nil + serializedExpected, err := yaml.Marshal(notificationPolicyExpected) + require.NoError(t, err) + + actual := apiClient.ExportNotificationPolicy(t, "yaml") + + require.YAMLEq(t, string(serializedExpected), actual) + }) + }) +} + +func TestIntegrationExportFileProvisionContactPoints(t *testing.T) { + dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + }) + + provisioningDir := filepath.Join(dir, "conf", "provisioning") + alertingDir := filepath.Join(provisioningDir, "alerting") + err := os.MkdirAll(alertingDir, 0750) + require.NoError(t, err) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p) + + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin") + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "admin", + Login: "admin", + IsAdmin: true, + }) + + apiClient.ReloadCachedPermissions(t) + t.Run("when provisioning contact points from files", func(t *testing.T) { + // add file provisioned contact points + fileProvisionedContactPoints, err := testData.ReadFile(path.Join("test-data", "provisioning-contact-points.yaml")) + require.NoError(t, err) + + var expected definitions.AlertingFileExport + require.NoError(t, yaml.Unmarshal(fileProvisionedContactPoints, &expected)) + expectedYaml, err := yaml.Marshal(expected) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(alertingDir, "provisioning-contact-points.yaml"), fileProvisionedContactPoints, 0750) + require.NoError(t, err) + + apiClient.ReloadAlertingFileProvisioning(t) + + t.Run("exported contact points should escape $ characters", func(t *testing.T) { + // call export endpoint + exportRaw := apiClient.ExportReceiver(t, "cp_1_$escaped", "yaml", true) + var export definitions.AlertingFileExport + require.NoError(t, yaml.Unmarshal([]byte(exportRaw), &export)) + + // verify the file exported matches the file provisioned thing + require.Len(t, export.ContactPoints, 1) + require.YAMLEq(t, string(expectedYaml), exportRaw) + }) + }) } diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index 5c101cb3768..568205ef36e 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -9,7 +9,6 @@ import ( "math/rand" "net/http" "path" - "regexp" "slices" "strings" "testing" @@ -121,6 +120,7 @@ func TestIntegrationAlertRulePermissions(t *testing.T) { pathsToIgnore := []string{ "GrafanaManagedAlert.Updated", + "GrafanaManagedAlert.UpdatedBy", "GrafanaManagedAlert.UID", "GrafanaManagedAlert.ID", "GrafanaManagedAlert.Data.Model", @@ -422,6 +422,7 @@ func TestIntegrationAlertRuleNestedPermissions(t *testing.T) { pathsToIgnore := []string{ "GrafanaManagedAlert.Updated", + "GrafanaManagedAlert.UpdatedBy", "GrafanaManagedAlert.UID", "GrafanaManagedAlert.ID", "GrafanaManagedAlert.Data.Model", @@ -1144,6 +1145,10 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { } }], "updated": "2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds": 60, "is_paused": false, "version": 1, @@ -1183,6 +1188,10 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { } }], "updated": "2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds": 60, "is_paused": false, "version": 1, @@ -1234,6 +1243,10 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { } }], "updated": "2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds": 60, "is_paused": false, "version": 1, @@ -1498,8 +1511,9 @@ func TestIntegrationRuleCreate(t *testing.T) { client.CreateFolder(t, namespaceUID, namespaceUID) cases := []struct { - name string - config apimodels.PostableRuleGroupConfig + name string + config apimodels.PostableRuleGroupConfig + expected apimodels.GettableRuleGroupConfig }{{ name: "can create a rule with UTF-8", config: apimodels.PostableRuleGroupConfig{ @@ -1514,8 +1528,7 @@ func TestIntegrationRuleCreate(t *testing.T) { "_bar1": "baz🙂", }, Annotations: map[string]string{ - "Προμηθέας": "prom", // Prometheus in Greek - "犬": "Shiba Inu", // Dog in Japanese + "Προμηθέας": "prom", // Prometheus in Greek }, }, GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ @@ -1536,6 +1549,52 @@ func TestIntegrationRuleCreate(t *testing.T) { }, }, }, + expected: apimodels.GettableRuleGroupConfig{ + Name: "test1", + Interval: model.Duration(time.Minute), + Rules: []apimodels.GettableExtendedRuleNode{ + { + ApiRuleNode: &apimodels.ApiRuleNode{ + For: util.Pointer(model.Duration(2 * time.Minute)), + Labels: map[string]string{ + "foo🙂": "bar", + "_bar1": "baz🙂", + }, + Annotations: map[string]string{ + "Προμηθέας": "prom", // Prometheus in Greek + }, + }, + GrafanaManagedAlert: &apimodels.GettableGrafanaRule{ + OrgID: 1, + Title: "test1 rule1", + Condition: "A", + Data: []apimodels.AlertQuery{ + { + RefID: "A", + RelativeTimeRange: apimodels.RelativeTimeRange{ + From: apimodels.Duration(0), + To: apimodels.Duration(15 * time.Minute), + }, + DatasourceUID: expr.DatasourceUID, + Model: json.RawMessage(`{"expression":"1","intervalMs":1000,"maxDataPoints":43200,"type":"math"}`), + }, + }, + UpdatedBy: &apimodels.UserInfo{ + Name: "admin", + }, + IntervalSeconds: 60, + Version: 1, + NamespaceUID: namespaceUID, + RuleGroup: "test1", + NoDataState: "NoData", + ExecErrState: "Alerting", + Provenance: "", + IsPaused: false, + Metadata: &apimodels.AlertRuleMetadata{}, + }, + }, + }, + }, }} for _, tc := range cases { @@ -1545,6 +1604,27 @@ func TestIntegrationRuleCreate(t *testing.T) { require.Len(t, resp.Created, 1) require.Len(t, resp.Updated, 0) require.Len(t, resp.Deleted, 0) + got, _, _ := client.GetRulesGroupWithStatus(t, namespaceUID, tc.config.Name) + + pathsToIgnore := []string{ + "GrafanaManagedAlert.Updated", + "GrafanaManagedAlert.UpdatedBy.UID", + "GrafanaManagedAlert.UID", + "GrafanaManagedAlert.ID", + "GrafanaManagedAlert.NamespaceID", + } + + // compare expected and actual and ignore the dynamic fields + diff := cmp.Diff(tc.expected, got.GettableRuleGroupConfig, cmp.FilterPath(func(path cmp.Path) bool { + for _, s := range pathsToIgnore { + if strings.HasSuffix(path.String(), s) { + return true + } + } + return false + }, cmp.Ignore())) + + require.Empty(t, diff) }) } } @@ -1696,6 +1776,18 @@ func TestIntegrationRuleUpdate(t *testing.T) { require.Equalf(t, http.StatusAccepted, status, "failed to post noop rule group. Response: %s", body) }) }) + t.Run("should set updated_by", func(t *testing.T) { + group := generateAlertRuleGroup(1, alertRuleGen()) + expected := model.Duration(10 * time.Second) + group.Rules[0].ApiRuleNode.For = &expected + + _, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group) + require.Equalf(t, http.StatusAccepted, status, "failed to post rule group. Response: %s", body) + getGroup := client.GetRulesGroup(t, folderUID, group.Name) + require.NotNil(t, getGroup.Rules[0].GrafanaManagedAlert.UpdatedBy) + assert.NotEmpty(t, getGroup.Rules[0].GrafanaManagedAlert.UpdatedBy.UID) + assert.Equal(t, "grafana", getGroup.Rules[0].GrafanaManagedAlert.UpdatedBy.Name) + }) } func TestIntegrationAlertAndGroupsQuery(t *testing.T) { @@ -2438,6 +2530,10 @@ func TestIntegrationQuota(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused": false, "version":2, @@ -2510,13 +2606,8 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) { require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) - - re := regexp.MustCompile(`"uid":"([\w|-]+)"`) - b = re.ReplaceAll(b, []byte(`"uid":""`)) - re = regexp.MustCompile(`"updated":"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)"`) - b = re.ReplaceAll(b, []byte(`"updated":"2021-05-19T19:47:55Z"`)) - - expectedGetRulesResponseBody := fmt.Sprintf(`{ + body, _ := rulesNamespaceWithoutVariableValues(t, b) + expectedGetRulesResponseBody := `{ "default": [ { "name": "arulegroup", @@ -2553,12 +2644,16 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) { } } ], - "updated": "2021-05-19T19:47:55Z", + "updated": "2021-02-21T01:10:30Z", + "updated_by" : { + "uid": "uid", + "name": "editor" + }, "intervalSeconds": 60, "is_paused": false, "version": 1, - "uid": "", - "namespace_uid": %q, + "uid": "uid", + "namespace_uid": "nsuid", "rule_group": "arulegroup", "no_data_state": "NoData", "exec_err_state": "Alerting", @@ -2573,8 +2668,8 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) { ] } ] - }`, namespaceUID) - assert.JSONEq(t, expectedGetRulesResponseBody, string(b)) + }` + assert.JSONEq(t, expectedGetRulesResponseBody, body) }) t.Run("editor can not delete the folder because it contains Grafana 8 alerts", func(t *testing.T) { u := fmt.Sprintf("http://editor:editor@%s/api/folders/%s", grafanaListedAddr, namespaceUID) @@ -3033,6 +3128,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused": false, "version":1, @@ -3075,6 +3174,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused": false, "version":1, @@ -3389,6 +3492,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused": false, "version":2, @@ -3504,6 +3611,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused":false, "version":3, @@ -3598,6 +3709,10 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { } ], "updated":"2021-02-21T01:10:30Z", + "updated_by": { + "uid": "uid", + "name": "grafana" + }, "intervalSeconds":60, "is_paused":false, "version":3, @@ -4289,6 +4404,7 @@ func rulesNamespaceWithoutVariableValues(t *testing.T, b []byte) (string, map[st rule.GrafanaManagedAlert.UID = "uid" rule.GrafanaManagedAlert.NamespaceUID = "nsuid" rule.GrafanaManagedAlert.Updated = time.Date(2021, time.Month(2), 21, 1, 10, 30, 0, time.UTC) + rule.GrafanaManagedAlert.UpdatedBy.UID = "uid" } } } diff --git a/pkg/tests/api/alerting/test-data/provisioning-contact-points.yaml b/pkg/tests/api/alerting/test-data/provisioning-contact-points.yaml new file mode 100644 index 00000000000..0a759626c4c --- /dev/null +++ b/pkg/tests/api/alerting/test-data/provisioning-contact-points.yaml @@ -0,0 +1,21 @@ +# config file version +apiVersion: 1 + +# List of contact points to import or update +contactPoints: + # organization ID, default = 1 + - orgId: 1 + # name of the contact point + name: cp_1_$$escaped + + receivers: + # unique identifier for the receiver. Should not exceed 40 symbols. Only letters, numbers, - (hyphen), and _ (underscore) allowed. + - uid: first_uid + # type of the receiver + type: prometheus-alertmanager + # Disable the additional [Incident Resolved] follow-up alert, default = false + disableResolveMessage: false + # settings for the specific receiver type + settings: + url: http://test:9000 + something: $$escaped diff --git a/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml b/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml new file mode 100644 index 00000000000..b8845d29548 --- /dev/null +++ b/pkg/tests/api/alerting/test-data/provisioning-mixed-set.yaml @@ -0,0 +1,159 @@ +# config file version +apiVersion: 1 + +contactPoints: + # organization ID, default = 1 + - orgId: 1 + # name of the contact point + name: $$xyz + + receivers: + # unique identifier for the receiver. Should not exceed 40 symbols. Only letters, numbers, - (hyphen), and _ (underscore) allowed. + - uid: first_uid + # type of the receiver + type: prometheus-alertmanager + # Disable the additional [Incident Resolved] follow-up alert, default = false + disableResolveMessage: false + # settings for the specific receiver type + settings: + url: http://test:9000 + something: $$escaped + +muteTimes: + # organization ID, default = 1 + - orgId: 1 + # name of the mute time interval, must be unique + name: $mute_time_1 + # time intervals that should trigger the muting + # refer to https://prometheus.io/docs/alerting/latest/configuration/#time_interval-0 + time_intervals: + - times: + - start_time: "06:00" + end_time: "23:59" + location: "UTC" + weekdays: ["monday:wednesday", "saturday", "sunday"] + months: ["1:3", "may:august", "december"] + years: ["2020:2022", "2030"] + days_of_month: ["1:5", "-3:-1"] + - orgId: 1 + # name of the mute time interval, must be unique + name: $mute_time_2 + # time intervals that should trigger the muting + # refer to https://prometheus.io/docs/alerting/latest/configuration/#time_interval-0 + time_intervals: + - times: + - start_time: "09:00" + end_time: "10:00" + location: "UTC" + weekdays: ["monday:wednesday", "saturday", "sunday"] + months: ["1:3", "may:august", "december"] + years: ["2020:2022", "2030"] + days_of_month: ["1:5", "-3:-1"] + +# ONLY THESE PATHS ARE NOT TEMPLATED and therefore don't need escaping: +# Alert rule annotations: groups[].rules[].annotations +# Alert rule time range: groups[].rules[].relativeTimeRange +# Alert rule query model: groups[].rules[].data.model +groups: + # organization ID, default = 1 + - orgId: 1 + # name of the rule group + name: my_rule_group + # name of the folder the rule group will be stored in + folder: my_first_folder_with_$$escaped_symbols + # interval that the rule group should evaluated at + interval: 60s + # list of rules that are part of the rule group + rules: + # unique identifier for the rule. Should not exceed 40 symbols. Only letters, numbers, - (hyphen), and _ (underscore) allowed. + - uid: my_id_1 + # title of the rule that will be displayed in the UI + title: my_first_rule_with_$$escaped_symbols + # which query should be used for the condition + condition: A + # list of query objects that should be executed on each + # evaluation - should be obtained through the API + data: + - refId: A + datasourceUid: "__expr__" + model: + conditions: + - evaluator: + params: + - 3 + type: gt + operator: + type: and + query: + params: + - A + reducer: + type: last + type: query + datasource: + type: __expr__ + uid: "__expr__" + expression: 1==0 + intervalMs: 1000 + maxDataPoints: 43200 + refId: A + type: math + # UID of a dashboard that the alert rule should be linked to + dashboardUid: my_dashboard + # ID of the panel that the alert rule should be linked to + panelId: 123 + # the state the alert rule will have when no data is returned + # possible values: "NoData", "Alerting", "OK", default = NoData + noDataState: Alerting + # the state the alert rule will have when the query execution + # failed - possible values: "Error", "Alerting", "OK" + # default = Alerting + execErrState: Alerting + # for how long should the alert fire before alerting + for: 60s + # > a map of strings to pass around any data + annotations: + some_key: some_value + $no_escaping_needed: $no_escaping_needed + # a map of strings that can be used to filter and + # route alerts + labels: + team: sre_team_1 + label_keys_not_$escaped: $$escaped_value + something: "escaped in the middle of things $$value" + templated: "{{ $$labels.team }}" + middle: "u$$ing_escaped_symbols" + notification_settings: + receiver: $$xyz + group_by: + - label_keys_not_$$escaped + - something + group_wait: 5m + group_interval: 10m + repeat_interval: 10m + mute_time_intervals: + - $mute_time_1 + - $mute_time_2 + +policies: + # organization ID, default = 1 + - orgId: 1 + # name of the contact point that should be used for this route + receiver: $$xyz + group_by: + - label_keys_not_$$escaped + # a list of prometheus-like matchers that an alert rule has to fulfill to match the node (allowed chars + # [a-zA-Z_:]) + matchers: + - alertname = Watchdog + - service_id_X = serviceX + - severity =~ "warning|critical" + # a list of grafana-like matchers that an alert rule has to fulfill to match the node + object_matchers: + - ["alertname", "=", "CPUUsage"] + - ["service_id-X", "=", "serviceX"] + - ["severity", "=~", "warning|critical"] + group_wait: 30s + group_interval: 5m + repeat_interval: 4h + routes: [] diff --git a/pkg/tests/api/alerting/test-data/provisioning-mute-times.yaml b/pkg/tests/api/alerting/test-data/provisioning-mute-times.yaml new file mode 100644 index 00000000000..3dc4d14872a --- /dev/null +++ b/pkg/tests/api/alerting/test-data/provisioning-mute-times.yaml @@ -0,0 +1,20 @@ +# config file version +apiVersion: 1 + +# ONLY THESE PATHS ARE NOT TEMPLATED and therefore don't need escaping: +# Mute timings name: muteTimes[].name +# Mute timings time intervals: muteTimes[].time_intervals[] +muteTimes: + - orgId: 1 + name: $mute_time_a + # time intervals that should trigger the muting + # refer to https://prometheus.io/docs/alerting/latest/configuration/#time_interval-0 + time_intervals: + - times: + - start_time: "06:00" + end_time: "23:59" + location: "UTC" + weekdays: ["monday:wednesday", "saturday", "sunday"] + months: ["1:3", "may:august", "december"] + years: ["2020:2022", "2030"] + days_of_month: ["1:5", "-3:-1"] diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index f599ec060a1..c66281d7af7 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -839,6 +839,32 @@ func (a apiClient) DeleteMuteTimingWithStatus(t *testing.T, name string) (int, s return resp.StatusCode, string(body) } +func (a apiClient) ExportMuteTiming(t *testing.T, name string, format string) string { + t.Helper() + + u, err := url.Parse(fmt.Sprintf("%s/api/v1/provisioning/mute-timings/%s/export", a.url, name)) + require.NoError(t, err) + q := url.Values{} + q.Set("format", format) + u.RawQuery = q.Encode() + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + require.NoError(t, err) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + + defer func() { + _ = resp.Body.Close() + }() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + requireStatusCode(t, http.StatusOK, resp.StatusCode, string(body)) + return string(body) +} + func (a apiClient) GetRouteWithStatus(t *testing.T) (apimodels.Route, int, string) { t.Helper() @@ -883,6 +909,32 @@ func (a apiClient) UpdateRouteWithStatus(t *testing.T, route apimodels.Route, no return resp.StatusCode, string(body) } +func (a apiClient) ExportNotificationPolicy(t *testing.T, format string) string { + t.Helper() + + u, err := url.Parse(fmt.Sprintf("%s/api/v1/provisioning/policies/export", a.url)) + require.NoError(t, err) + q := url.Values{} + q.Set("format", format) + u.RawQuery = q.Encode() + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + require.NoError(t, err) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + + defer func() { + _ = resp.Body.Close() + }() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + requireStatusCode(t, http.StatusOK, resp.StatusCode, string(body)) + return string(body) +} + func (a apiClient) UpdateRoute(t *testing.T, route apimodels.Route, noProvenance bool) { t.Helper() status, data := a.UpdateRouteWithStatus(t, route, noProvenance) diff --git a/pkg/tests/apis/core/openapi_test.go b/pkg/tests/apis/core/openapi_test.go new file mode 100644 index 00000000000..c861f54f4fb --- /dev/null +++ b/pkg/tests/apis/core/openapi_test.go @@ -0,0 +1,120 @@ +package core + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/version" + apimachineryversion "k8s.io/apimachinery/pkg/version" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func TestIntegrationOpenAPIs(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + check := []schema.GroupVersion{{ + Group: "dashboard.grafana.app", + Version: "v0alpha1", + }, { + Group: "folder.grafana.app", + Version: "v0alpha1", + }, { + Group: "peakq.grafana.app", + Version: "v0alpha1", + }} + + h := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesFoldersServiceV2, // Will be default on by G12 + featuremgmt.FlagQueryService, // Query Library + }, + }) + + t.Run("check valid version response", func(t *testing.T) { + disco := h.NewDiscoveryClient() + req := disco.RESTClient().Get(). + Prefix("version"). + SetHeader("Accept", "application/json") + + result := req.Do(context.Background()) + require.NoError(t, result.Error()) + + raw, err := result.Raw() + require.NoError(t, err) + info := apimachineryversion.Info{} + err = json.Unmarshal(raw, &info) + require.NoError(t, err) + + // Make sure the gitVersion is parsable + v, err := version.Parse(info.GitVersion) + require.NoError(t, err) + require.Equal(t, info.Major, fmt.Sprintf("%d", v.Major())) + require.Equal(t, info.Minor, fmt.Sprintf("%d", v.Minor())) + }) + + t.Run("build open", func(t *testing.T) { + // Now write each OpenAPI spec to a static file + dir := filepath.Join("..", "..", "..", "..", "openapi") + for _, gv := range check { + path := fmt.Sprintf("/openapi/v3/apis/%s/%s", gv.Group, gv.Version) + rsp := apis.DoRequest(h, apis.RequestParams{ + Method: http.MethodGet, + Path: path, + User: h.Org1.Admin, + }, &apis.AnyResource{}) + + require.NotNil(t, rsp.Response) + require.Equal(t, 200, rsp.Response.StatusCode, path) + + var prettyJSON bytes.Buffer + err := json.Indent(&prettyJSON, rsp.Body, "", " ") + require.NoError(t, err) + pretty := prettyJSON.String() + + write := false + fpath := filepath.Join(dir, fmt.Sprintf("%s-%s.json", gv.Group, gv.Version)) + + // nolint:gosec + // We can ignore the gosec G304 warning since this is a test and the function is only called with explicit paths + body, err := os.ReadFile(fpath) + if err == nil { + if !assert.JSONEq(t, string(body), pretty) { + t.Logf("openapi spec has changed: %s", path) + t.Fail() + write = true + } + } else { + t.Errorf("missing openapi spec for: %s", path) + write = true + } + + if write { + e2 := os.WriteFile(fpath, []byte(pretty), 0644) + if e2 != nil { + t.Errorf("error writing file: %s", e2.Error()) + } + } + } + }) +} diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index 54a629a82f8..89cdc2b941d 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -22,9 +22,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer/yaml" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/version" yamlutil "k8s.io/apimachinery/pkg/util/yaml" - apimachineryversion "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" @@ -653,31 +651,6 @@ func (c *K8sTestHelper) NewDiscoveryClient() *discovery.DiscoveryClient { return client } -func (c *K8sTestHelper) GetVersionInfo() apimachineryversion.Info { - c.t.Helper() - - disco := c.NewDiscoveryClient() - req := disco.RESTClient().Get(). - Prefix("version"). - SetHeader("Accept", "application/json") - - result := req.Do(context.Background()) - require.NoError(c.t, result.Error()) - - raw, err := result.Raw() - require.NoError(c.t, err) - info := apimachineryversion.Info{} - err = json.Unmarshal(raw, &info) - require.NoError(c.t, err) - - // Make sure the gitVersion is parsable - v, err := version.Parse(info.GitVersion) - require.NoError(c.t, err) - require.Equal(c.t, info.Major, fmt.Sprintf("%d", v.Major())) - require.Equal(c.t, info.Minor, fmt.Sprintf("%d", v.Minor())) - return info -} - func (c *K8sTestHelper) GetGroupVersionInfoJSON(group string) string { c.t.Helper() diff --git a/pkg/tests/apis/playlist/playlist_test.go b/pkg/tests/apis/playlist/playlist_test.go index fa93da02e26..49ee9fee4ca 100644 --- a/pkg/tests/apis/playlist/playlist_test.go +++ b/pkg/tests/apis/playlist/playlist_test.go @@ -49,9 +49,6 @@ func TestIntegrationPlaylist(t *testing.T) { EnableFeatureToggles: []string{}, })) - // Ensure the k8s version is valid - _ = h.GetVersionInfo() - // The accepted verbs will change when dual write is enabled disco := h.GetGroupVersionInfoJSON("playlist.grafana.app") // fmt.Printf("%s", disco) diff --git a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx index 582e0855008..7d93518167e 100644 --- a/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx +++ b/public/app/features/dashboard-scene/inspect/InspectJsonTab.test.tsx @@ -223,7 +223,7 @@ async function buildTestSceneWithLibraryPanel() { title: 'Panel A', pluginId: 'table', key: 'panel-12', - $behaviors: [new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: '111' })], + $behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: '111' })], titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], $data: new SceneDataTransformer({ transformations: [ diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts index f204eae7138..fa309c8bd21 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts @@ -200,6 +200,7 @@ describe('PanelEditor', () => { const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: true, + title: libraryPanelModel.title, uid: libraryPanelModel.uid, name: libraryPanelModel.name, _loadedPanel: libraryPanelModel, @@ -238,7 +239,7 @@ describe('PanelEditor', () => { // Wait for mock api to return and update the library panel expect(libPanelBehavior.state._loadedPanel?.version).toBe(2); expect(libPanelBehavior.state.name).toBe('changed name'); - expect(panel.state.title).toBe('changed title'); + expect(libPanelBehavior.state.title).toBe('changed title'); expect((gridItem.state.body as VizPanel).state.title).toBe('changed title'); }); @@ -257,6 +258,7 @@ describe('PanelEditor', () => { const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: true, + title: libraryPanelModel.title, uid: libraryPanelModel.uid, name: libraryPanelModel.name, _loadedPanel: libraryPanelModel, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx index 3f54a519b70..264c053de17 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx @@ -177,6 +177,7 @@ describe('PanelOptions', () => { const libraryPanel = new LibraryPanelBehavior({ isLoaded: true, + title: libraryPanelModel.title, uid: libraryPanelModel.uid, name: libraryPanelModel.name, _loadedPanel: libraryPanelModel, diff --git a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx index e8a0054a731..f298e7b19f9 100644 --- a/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx +++ b/public/app/features/dashboard-scene/scene/AddLibraryPanelDrawer.test.tsx @@ -96,7 +96,7 @@ describe('AddLibraryPanelWidget', () => { title: 'Panel Title', pluginId: 'table', key: 'panel-1', - $behaviors: [new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: 'uid' })], }); addLibPanelDrawer = new AddLibraryPanelDrawer({ panelToReplaceRef: libPanel.getRef() }); diff --git a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx index 1b183577b1f..393f3cfaea7 100644 --- a/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardDatasourceBehaviour.test.tsx @@ -408,6 +408,7 @@ describe('DashboardDatasourceBehaviour', () => { it('should re-run queries when library panel re-runs query', async () => { const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: false, + title: 'Panel title', uid: 'fdcvggvfy2qdca', name: 'My Library Panel', _loadedPanel: undefined, @@ -468,6 +469,7 @@ describe('DashboardDatasourceBehaviour', () => { jest.spyOn(console, 'error').mockImplementation(); const libPanelBehavior = new LibraryPanelBehavior({ isLoaded: false, + title: 'Panel title', uid: 'fdcvggvfy2qdca', name: 'My Library Panel', _loadedPanel: undefined, @@ -517,6 +519,7 @@ describe('DashboardDatasourceBehaviour', () => { // Simulate library panel being loaded libPanelBehavior.setState({ isLoaded: true, + title: 'Panel title', uid: 'fdcvggvfy2qdca', name: 'My Library Panel', _loadedPanel: undefined, diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 80d4b19fa3a..2c54e174bea 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -468,7 +468,7 @@ describe('DashboardScene', () => { title: 'Library Panel', pluginId: 'table', key: 'panel-4', - $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], }); scene.copyPanel(libVizPanel); @@ -523,7 +523,7 @@ describe('DashboardScene', () => { title: 'Library Panel', pluginId: 'table', key: 'panel-4', - $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], }), }) ); @@ -542,7 +542,7 @@ describe('DashboardScene', () => { const libPanel = new VizPanel({ title: 'Panel B', pluginId: 'table', - $behaviors: [new LibraryPanelBehavior({ name: 'lib panel', uid: 'abc', isLoaded: true })], + $behaviors: [new LibraryPanelBehavior({ title: 'title', name: 'lib panel', uid: 'abc', isLoaded: true })], }); const scene = buildTestScene({ @@ -907,7 +907,7 @@ function buildTestScene(overrides?: Partial) { title: 'Library Panel', pluginId: 'table', key: 'panel-5', - $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], }), }), ], @@ -925,7 +925,7 @@ function buildTestScene(overrides?: Partial) { title: 'Library Panel', pluginId: 'table', key: 'panel-6', - $behaviors: [new LibraryPanelBehavior({ name: 'libraryPanel', uid: 'uid' })], + $behaviors: [new LibraryPanelBehavior({ title: 'Library Panel', name: 'libraryPanel', uid: 'uid' })], }), }), ], diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx index b6eb6872566..02a363d5d3a 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.test.tsx @@ -167,7 +167,7 @@ describe('LibraryPanelBehavior', () => { }); async function buildTestSceneWithLibraryPanel() { - const behavior = new LibraryPanelBehavior({ name: 'LibraryPanel A', uid: '111' }); + const behavior = new LibraryPanelBehavior({ title: 'LibraryPanel A title', name: 'LibraryPanel A', uid: '111' }); const vizPanel = new VizPanel({ title: 'Panel A', diff --git a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx index 44a1c0de5a8..423cc2c34ee 100644 --- a/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/LibraryPanelBehavior.tsx @@ -17,6 +17,8 @@ import { AngularDeprecation } from './angular/AngularDeprecation'; import { DashboardGridItem } from './layout-default/DashboardGridItem'; export interface LibraryPanelBehaviorState extends SceneObjectState { + // Library panels use title from dashboard JSON's panel model, not from library panel definition, hence we pass it. + title?: string; uid: string; name: string; isLoaded?: boolean; @@ -64,7 +66,7 @@ export class LibraryPanelBehavior extends SceneObjectBase { $behaviors: [ new LibraryPanelBehavior({ name: 'Some lib panel panel', + title: 'A panel', uid: 'lib-panel-uid', }), ], @@ -398,7 +399,7 @@ describe('transformSceneToSaveModel', () => { x: 0, y: 0, }); - expect(result.title).toBe('Panel blahh blah'); + expect(result.title).toBe('A panel'); expect(result.transformations).toBeUndefined(); expect(result.fieldConfig).toBeUndefined(); expect(result.options).toBeUndefined(); @@ -850,6 +851,7 @@ describe('transformSceneToSaveModel', () => { $behaviors: [ new LibraryPanelBehavior({ name: 'Some lib panel panel', + title: 'A panel', uid: 'lib-panel-uid', }), ], @@ -863,7 +865,7 @@ describe('transformSceneToSaveModel', () => { expect(result[0]).toMatchObject({ id: 4, - title: 'Panel blahh blah', + title: 'A panel', libraryPanel: { name: 'Some lib panel panel', uid: 'lib-panel-uid', diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index c1b6c96ec07..4c8368fc1fd 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -188,7 +188,7 @@ export function vizPanelToPanel( panel = { id: getPanelIdForVizPanel(vizPanel), - title: vizPanel.state.title, + title: libPanel!.state.title, gridPos: gridPos, libraryPanel: { name: libPanel!.state.name, diff --git a/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts b/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts index 02dcc897936..e6e44c7a583 100644 --- a/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts +++ b/public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.test.ts @@ -17,6 +17,7 @@ describe('PanelModelCompatibilityWrapper', () => { const libPanel = new LibraryPanelBehavior({ uid: 'a', name: 'aa', + title: 'a', }); panel.setState({ diff --git a/public/app/features/logs/components/LogRow.tsx b/public/app/features/logs/components/LogRow.tsx index 49a8f86c32a..30fc8c773ba 100644 --- a/public/app/features/logs/components/LogRow.tsx +++ b/public/app/features/logs/components/LogRow.tsx @@ -14,6 +14,7 @@ import { import { reportInteraction } from '@grafana/runtime'; import { DataQuery, TimeZone } from '@grafana/schema'; import { Icon, PopoverContent, Tooltip, useTheme2 } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; import { checkLogsError, checkLogsSampled, escapeUnescapedString } from '../utils'; @@ -244,7 +245,13 @@ export const LogRow = ({ className={enableLogDetails ? styles.logsRowToggleDetails : ''} > {enableLogDetails && ( - + )} {showTime && {timestamp}} diff --git a/public/app/features/logs/components/LogRowMessage.tsx b/public/app/features/logs/components/LogRowMessage.tsx index f771d9a2bb6..d3788b3c7cf 100644 --- a/public/app/features/logs/components/LogRowMessage.tsx +++ b/public/app/features/logs/components/LogRowMessage.tsx @@ -157,6 +157,7 @@ export const LogRowMessage = memo((props: Props) => { [raw, prettifyLogMessage, wrapLogMessage, expanded] ); const shouldShowMenu = mouseIsOver || pinned; + return ( <> { @@ -165,9 +166,9 @@ export const LogRowMessage = memo((props: Props) => { }
- +
diff --git a/public/app/features/logs/components/getLogRowStyles.ts b/public/app/features/logs/components/getLogRowStyles.ts index 9b965b65ea1..8605753487e 100644 --- a/public/app/features/logs/components/getLogRowStyles.ts +++ b/public/app/features/logs/components/getLogRowStyles.ts @@ -100,6 +100,11 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => { cursor: 'pointer', verticalAlign: 'top', + '&:focus-within': { + outline: `2px solid ${theme.colors.primary.border}`, + outlineOffset: '-2px', + }, + '&:hover': { '.log-row-menu': { zIndex: 1, @@ -139,7 +144,6 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => { logsRowToggleDetails: css({ label: 'logs-row-toggle-details__level', fontSize: '9px', - paddingTop: '5px', maxWidth: '15px', }), logsRowLocalTime: css({ @@ -221,6 +225,29 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => { backgroundColor: hoverBgColor, }, }), + detailsToggle: css({ + appearance: 'none', + background: 'none', + border: 'none', + padding: 0, + // Don't increase the height of the row + maxHeight: '19px', + + // Don't show default button box-shadow on focus, we apply outline to the entire row instead + '&:focus-visible': { + boxShadow: 'none', + }, + + '&:focus': { + outline: 0, + }, + '&:after': { + content: '""', + inset: 0, + position: 'absolute', + }, + }), + // Log row topVerticalAlign: css({ label: 'topVerticalAlign', @@ -287,9 +314,6 @@ export const getLogRowStyles = memoizeOne((theme: GrafanaTheme2) => { }, }), logLine: css({ - backgroundColor: 'transparent', - border: 'none', - diplay: 'inline', fontFamily: theme.typography.fontFamilyMonospace, fontSize: theme.typography.bodySmall.fontSize, letterSpacing: theme.typography.bodySmall.letterSpacing, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 979924df896..f8d8d6cf171 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1815,7 +1815,8 @@ }, "log-row-message": { "ellipsis": "… ", - "more": "more" + "more": "more", + "see-details": "See log details" }, "log-rows": { "disable-popover": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index d155653d0ed..a485a54fac9 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1815,7 +1815,8 @@ }, "log-row-message": { "ellipsis": "… ", - "more": "mőřę" + "more": "mőřę", + "see-details": "Ŝęę ľőģ đęŧäįľş" }, "log-rows": { "disable-popover": { diff --git a/yarn.lock b/yarn.lock index 46218dc6903..4c42d41e41d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17970,7 +17970,7 @@ __metadata: node-notifier: "npm:10.0.1" nx: "npm:19.8.2" ol: "npm:7.4.0" - ol-ext: "npm:4.0.24" + ol-ext: "npm:4.0.25" pluralize: "npm:^8.0.0" postcss: "npm:8.5.1" postcss-loader: "npm:8.1.1" @@ -18010,7 +18010,7 @@ __metadata: react-virtual: "npm:2.10.4" react-virtualized-auto-sizer: "npm:1.0.25" react-window: "npm:1.8.11" - react-window-infinite-loader: "npm:1.0.9" + react-window-infinite-loader: "npm:1.0.10" react-zoom-pan-pinch: "npm:^3.3.0" reduce-reducers: "npm:^1.0.4" redux: "npm:5.0.1" @@ -18019,7 +18019,7 @@ __metadata: regenerator-runtime: "npm:0.14.1" reselect: "npm:5.1.1" rimraf: "npm:6.0.1" - rudder-sdk-js: "npm:2.48.43" + rudder-sdk-js: "npm:2.48.44" rxjs: "npm:7.8.1" sass: "npm:1.83.4" sass-loader: "npm:16.0.4" @@ -20940,8 +20940,8 @@ __metadata: linkType: hard "knip@npm:^5.10.0": - version: 5.43.1 - resolution: "knip@npm:5.43.1" + version: 5.43.5 + resolution: "knip@npm:5.43.5" dependencies: "@nodelib/fs.walk": "npm:3.0.1" "@snyk/github-codeowners": "npm:1.1.0" @@ -20965,7 +20965,7 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 10/068e4145371cf3a4434d07a206eddf8f1d509541482d76252440484562f0b989c11c3efb9c4083d8b5854a90758d3bbcc4a228fe935f6e90ecc9ef2c9f9da8a7 + checksum: 10/d6d7b6561b07fc1696aa109c822c4653c4cfdfd4aafb3bbb27e508415ff8b6e7d2fd14158cea800d59aa783e8fee25c641d1348374af86f743e2dee8140722d0 languageName: node linkType: hard @@ -23335,12 +23335,12 @@ __metadata: languageName: node linkType: hard -"ol-ext@npm:4.0.24": - version: 4.0.24 - resolution: "ol-ext@npm:4.0.24" +"ol-ext@npm:4.0.25": + version: 4.0.25 + resolution: "ol-ext@npm:4.0.25" peerDependencies: ol: ">= 5.3.0" - checksum: 10/76ab4ee468429171be7495998c0ae80853733f9ff6241dfd7b66208f265e180357bb80c304b2d185c50a4afa635a36ed8ed67fcd5374b49d790e71b9be039df9 + checksum: 10/e3c8282fc67d9511b37c540f97217594a28f16b7ebeee5baf828eb56fdefa024fad6c1f846ac3cfd093f3b1e2cab5b6a56a97087152389fe4e35a2eb9b42f415 languageName: node linkType: hard @@ -26553,13 +26553,13 @@ __metadata: languageName: node linkType: hard -"react-window-infinite-loader@npm:1.0.9": - version: 1.0.9 - resolution: "react-window-infinite-loader@npm:1.0.9" +"react-window-infinite-loader@npm:1.0.10": + version: 1.0.10 + resolution: "react-window-infinite-loader@npm:1.0.10" peerDependencies: - react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 - react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 - checksum: 10/4a11ad949443cd76f9304d0558516c53474f53ca370e54bc29c2c362657b43545c89aec3e7df42d8e8511df379aed79b6fdd2c8fc52e25a9559b64e41ac7aefe + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/4f4c097a2948f8da71d13199289d85c89720e41888ea50039d2b8e6d7cf160300e97f66421c53420d8cb95b3ece74940c07ebd23bddd7a705a679d7e2dc2957e languageName: node linkType: hard @@ -27482,10 +27482,10 @@ __metadata: languageName: node linkType: hard -"rudder-sdk-js@npm:2.48.43": - version: 2.48.43 - resolution: "rudder-sdk-js@npm:2.48.43" - checksum: 10/183888b1bd922d3e811fae5729b6a0ac93d5dbb44e8ee753ced80ae496d19c9127ef8de8908718d43ed4b33e4e9b738c3bbc562ea6cb02c050702f9b850ae2a5 +"rudder-sdk-js@npm:2.48.44": + version: 2.48.44 + resolution: "rudder-sdk-js@npm:2.48.44" + checksum: 10/538bc405c0cb7faf33b910a39d51bd6df53fbdb176d4e83c33821e7a454178b65ced216ad0f055b6dffd201ccc45cead814138e20683e15424a02fb54b894e15 languageName: node linkType: hard