Merge branch 'main' of github.com:grafana/grafana into alerting/81308-alertmanager-label-quotes

This commit is contained in:
Konrad Lalik
2024-02-02 08:16:51 +01:00
447 changed files with 14119 additions and 5073 deletions
+4 -11
View File
@@ -1050,9 +1050,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
"public/app/core/components/SharedPreferences/SharedPreferences.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/core/components/TagFilter/TagFilter.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@@ -2442,7 +2439,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"]
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"]
],
"public/app/features/dashboard-scene/settings/variables/components/VariableSelectField.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
@@ -3477,8 +3475,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/explore/TraceView/components/model/link-patterns.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/explore/TraceView/components/model/link-patterns.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@@ -3492,8 +3489,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Do not use any type assertions.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Do not use any type assertions.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"]
[0, 0, 0, "Do not use any type assertions.", "11"]
],
"public/app/features/explore/TraceView/components/model/transform-trace-data.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
@@ -4329,9 +4325,6 @@ exports[`better eslint`] = {
"public/app/features/variables/datasource/actions.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/variables/editor/LegacyVariableQueryEditor.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"public/app/features/variables/editor/VariableEditorContainer.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
+12 -1
View File
@@ -123,6 +123,7 @@ steps:
image: alpine:3.18.5
name: identify-runner
- commands:
- apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python
- yarn install --immutable
depends_on: []
image: node:20.9.0-alpine
@@ -224,6 +225,7 @@ steps:
image: alpine:3.18.5
name: identify-runner
- commands:
- apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python
- yarn install --immutable
depends_on: []
image: node:20.9.0-alpine
@@ -535,6 +537,7 @@ steps:
image: golang:1.21.6-alpine
name: wire-install
- commands:
- apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python
- yarn install --immutable
depends_on: []
image: node:20.9.0-alpine
@@ -1044,6 +1047,7 @@ steps:
image: alpine:3.18.5
name: identify-runner
- commands:
- apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python
- yarn install --immutable
depends_on: []
image: node:20.9.0-alpine
@@ -1403,6 +1407,7 @@ steps:
image: alpine:3.18.5
name: identify-runner
- commands:
- apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python
- yarn install --immutable
depends_on: []
image: node:20.9.0-alpine
@@ -1479,6 +1484,7 @@ steps:
image: alpine:3.18.5
name: identify-runner
- commands:
- apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python
- yarn install --immutable
depends_on: []
image: node:20.9.0-alpine
@@ -1537,6 +1543,7 @@ steps:
image: alpine:3.18.5
name: identify-runner
- commands:
- apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python
- yarn install --immutable
depends_on: []
image: node:20.9.0-alpine
@@ -1799,6 +1806,7 @@ steps:
image: golang:1.21.6-alpine
name: wire-install
- commands:
- apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python
- yarn install --immutable
depends_on: []
image: node:20.9.0-alpine
@@ -2714,6 +2722,7 @@ steps:
image: golang:1.21.6-alpine
name: compile-build-cmd
- commands:
- apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python
- yarn install --immutable
depends_on: []
image: node:20.9.0-alpine
@@ -2987,6 +2996,7 @@ steps:
image: alpine:3.18.5
name: identify-runner
- commands:
- apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python
- yarn install --immutable
depends_on: []
image: node:20.9.0-alpine
@@ -3410,6 +3420,7 @@ steps:
image: alpine:3.18.5
name: identify-runner
- commands:
- apk add --update g++ make python3 && ln -sf /usr/bin/python3 /usr/bin/python
- yarn install --immutable
depends_on: []
image: node:20.9.0-alpine
@@ -4777,6 +4788,6 @@ kind: secret
name: gcr_credentials
---
kind: signature
hmac: 0e34c95370617ee9f721421913cbe1fe103c117e2912ac589953298246fd2012
hmac: c04928dc37abd22bc2d48c971f4f1fdfd722d38629016139fd0304e2aef60213
...
+5 -3
View File
@@ -111,7 +111,9 @@
"public/app/plugins/datasource/parca/*.{ts,tsx}",
"public/app/plugins/datasource/parca/**/*.{ts,tsx}",
"public/app/plugins/datasource/tempo/*.{ts,tsx}",
"public/app/plugins/datasource/tempo/**/*.{ts,tsx}"
"public/app/plugins/datasource/tempo/**/*.{ts,tsx}",
"public/app/plugins/datasource/loki/*.{ts,tsx}",
"public/app/plugins/datasource/loki/**/*.{ts,tsx}"
],
"settings": {
"import/resolver": {
@@ -127,8 +129,8 @@
"zones": [
{
"target": "./public/app/plugins",
"from": "./public/app",
"except": ["./plugins"],
"from": "./public",
"except": ["./app/plugins"],
"message": "Core plugins are not allowed to depend on Grafana core packages"
}
]
+11 -11
View File
@@ -108,7 +108,7 @@
/pkg/services/dashboardversion/ @grafana/backend-platform
/pkg/services/encryption/ @grafana/backend-platform
/pkg/services/folder/ @grafana/backend-platform
/pkg/services/grafana-apiserver @grafana/grafana-app-platform-squad
/pkg/services/apiserver @grafana/grafana-app-platform-squad
/pkg/services/hooks/ @grafana/backend-platform
/pkg/services/kmsproviders/ @grafana/backend-platform
/pkg/services/licensing/ @grafana/backend-platform
@@ -275,7 +275,6 @@
/pkg/modules/ @grafana/grafana-app-platform-squad
/pkg/kindsysreport/ @grafana/grafana-app-platform-squad
/pkg/services/grpcserver/ @grafana/grafana-app-platform-squad
/pkg/aggregator @grafana/grafana-app-platform-squad
/pkg/generated @grafana/grafana-app-platform-squad
# Alerting
@@ -344,9 +343,9 @@
/packages/grafana-sql/ @grafana/partner-datasources @grafana/oss-big-tent
# root files, mostly frontend
.browserslistrc @grafana/frontend-ops
package.json @grafana/frontend-ops
tsconfig.json @grafana/frontend-ops
/.browserslistrc @grafana/frontend-ops
/package.json @grafana/frontend-ops
/tsconfig.json @grafana/frontend-ops
/.editorconfig @grafana/frontend-ops
/.eslintignore @grafana/frontend-ops
/.gitattributes @grafana/frontend-ops
@@ -356,7 +355,7 @@ tsconfig.json @grafana/frontend-ops
/.yarn @grafana/frontend-ops
/.yarnrc.yml @grafana/frontend-ops
/yarn.lock @grafana/frontend-ops
lerna.json @grafana/frontend-ops
/lerna.json @grafana/frontend-ops
/.prettierrc.js @grafana/frontend-ops
/.eslintrc @grafana/frontend-ops
/.vim @zoltanbedi
@@ -367,9 +366,9 @@ lerna.json @grafana/frontend-ops
/tools/ @grafana/frontend-ops
/lefthook.yml @grafana/frontend-ops
/lefthook.rc @grafana/frontend-ops
.husky/pre-commit @grafana/frontend-ops
cypress.config.js @grafana/grafana-frontend-platform
.levignore.js @grafana/plugins-platform-frontend
/.husky/pre-commit @grafana/frontend-ops
/cypress.config.js @grafana/grafana-frontend-platform
/.levignore.js @grafana/plugins-platform-frontend
# public folder
@@ -382,7 +381,7 @@ cypress.config.js @grafana/grafana-frontend-platform
/public/app/features/all.ts @grafana/grafana-frontend-platform
/public/app/features/admin/ @grafana/identity-access-team
/public/app/features/auth-config/ @grafana/identity-access-team
/public/app/features/annotations/ @grafana/grafana-frontend-platform
/public/app/features/annotations/ @grafana/dashboards-squad
/public/app/features/api-keys/ @grafana/identity-access-team
/public/app/features/canvas/ @grafana/dataviz-squad
/public/app/features/geo/ @grafana/dataviz-squad
@@ -678,7 +677,8 @@ embed.go @grafana/grafana-as-code
/.github/workflows/ephemeral-instances-pr-opened-closed.yml @grafana/grafana-operator-experience-squad
/.github/workflows/create-security-patch-from-security-mirror.yml @grafana/grafana-release-guild
/.github/workflows/core-plugins-build-and-release.yml @grafana/plugins-platform-frontend @grafana/plugins-platform-backend
/.github/workflows/i18n-crowdin-fix-files.yml @grafana/grafana-frontend-platform
/.github/workflows/i18n-crowdin-upload.yml @grafana/grafana-frontend-platform
/.github/workflows/i18n-crowdin-download.yml @grafana/grafana-frontend-platform
/.github/workflows/feature-toggle-cleanup.yml @tolzhabayev
/.github/workflows/scripts/feature-toggle-cleanup/feature-toggle-cleanup.js @tolzhabayev
@@ -0,0 +1,51 @@
name: Crowdin Download Action
on:
workflow_dispatch:
schedule:
- cron: "0 * * * *"
jobs:
download-sources-from-crowdin:
runs-on: ubuntu-latest
permissions:
contents: write # needed to commit changes into the PR
pull-requests: write # needed to update PR description, labels, etc
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- name: Download sources
uses: crowdin/github-action@v1
with:
upload_sources: false
upload_translations: false
download_sources: false
download_translations: true
localization_branch_name: i18n_crowdin_translations
create_pull_request: true
pull_request_title: 'I18n: Download translations from Crowdin'
pull_request_body: |
:robot: Automatic download of translations from Crowdin.
Steps for merging:
1. A quick sanity check of the changes and approve. Things to look out for:
- No changes in the English file. The source of truth is in the main branch, NOT in Crowdin.
- Translations maybe be removed if the English phrase was removed, but there should not be many of these
- Anything else that looks 'funky'. Ask if you're not sure.
2. Approve & (Auto-)merge. :tada:
If there's a conflict, close the pull request and **delete the branch**. A GH action will recreate the pull request.
Remember, the longer this pull request is open, the more likely it is that it'll get conflicts.
pull_request_labels: 'area/frontend, area/internationalization, no-changelog, no-backport'
pull_request_reviewers: 'grafana-frontend-platform'
pull_request_base_branch_name: 'main'
base_url: 'https://grafana.api.crowdin.com'
config: 'crowdin.yml'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
@@ -1,67 +0,0 @@
# When Crowdin creates a pull request from the crowdin-service-branch branch,
# run `yarn i18n:extract` and commit the changed grafana.json files back into the PR
# to reformat crowdin's changes to prevent conflicts with our CI checks.
name: Fix Crowdin I18n files
on:
pull_request:
paths:
- 'public/locales/*/grafana.json'
branches:
- main # Only run on pull requests *target* main (will be merged into main)
jobs:
fix-files:
# Only run on pull requests *from* the crowdin-service-branch branch
if: github.head_ref == 'crowdin-service-branch'
name: Fix files
runs-on: ubuntu-latest
permissions:
contents: write # needed to commit changes back into the PR
pull-requests: write # needed to update PR description
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- uses: actions/setup-node@v4
with:
node-version: 20.9.0
cache: 'yarn'
- name: Install dependencies
run: yarn install
- name: Extract I18n files
run: yarn i18n:extract
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@8756aa072ef5b4a080af5dc8fef36c5d586e521d # v5.0.0
with:
commit_message: "Github Action: Auto-fix i18n files"
file_pattern: public/locales/*/grafana.json
- name: Update PR description
uses: devindford/Append_PR_Comment@32dd2619cd96ac8da9907c416c992fe265233ca8 # v1.1.3
if: ${{ ! contains(github.event.pull_request.body, 'Steps for merging') }}
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
body-update-action: prefix
body-template: |
:robot: Automatic sync of translations from Crowdin.
Steps for merging:
1. Wait for the "Github Action: Auto-fix i18n files" commit that may be required for CI to pass.
2. A quick sanity check of the changes and approve. Things to look out for:
- No changes to the English strings. The source of truth is already in the main branch, NOT Crowdin.
- Translations maybe be removed if the English phrase was removed, but there should not be many of these
- Anything else that looks 'funky'. Ask if you're not sure.
3. Approve & (Auto-)merge. :tada:
If there's a conflict, close the pull request and **delete the branch**. Crowdin will recreate the pull request eventually.
Remember, the longer this pull request is open, the more likely it is that it'll get conflicts.
+29
View File
@@ -0,0 +1,29 @@
name: Crowdin Upload Action
on:
push:
paths:
- 'public/locales/en-US/grafana.json'
branches:
- main
jobs:
upload-sources-to-crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Upload sources
uses: crowdin/github-action@v1
with:
upload_sources: true
upload_translations: false
download_translations: false
create_pull_request: false
base_url: 'https://grafana.api.crowdin.com'
config: 'crowdin.yml'
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
+4
View File
@@ -33,6 +33,10 @@ packageExtensions:
react-resizable@3.0.4:
peerDependencies:
react-dom: 17.0.1
'@msagl/drawing@*':
dependencies:
queue-typescript: "^1.0.1"
"@esfx/collections-sortedmap": "^1.0.0"
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
+5
View File
@@ -1047,6 +1047,11 @@ instrumentations_console_enabled = false
# Should webvitals instrumentation be enabled, only affects Grafana Javascript Agent
instrumentations_webvitals_enabled = false
# level of internal logging for debugging Grafana Javascript Agent.
# possible values are: 0 = OFF, 1 = ERROR, 2 = WARN, 3 = INFO, 4 = VERBOSE
# more details: https://github.com/grafana/faro-web-sdk/blob/v1.3.7/docs/sources/tutorials/quick-start-browser.md#how-to-activate-debugging
internal_logger_level = 0
# Api Key, only applies to Grafana Javascript Agent provider
api_key =
+11 -9
View File
@@ -9,7 +9,8 @@ Grafana uses the [i18next](https://www.i18next.com/) framework for managing tran
- Use `<Trans i18nKey="search-results.panel-link">Go to {{ pageTitle }}</Trans>` in code to add a translatable phrase
- Translations are stored in JSON files in `public/locales/{locale}/grafana.json`
- If a particular phrase is not available in the a language then it will fall back to English
- To update phrases in English, edit the default phrase in both the component's source and the [English grafana.json message catalogue](../public/locales/en-US/grafana.json), then run `yarn i18n:extract`.
- To update phrases in English, edit the default phrase in the component's source and then run `yarn i18n:extract`.
- The single source of truth for en-US (fallback language) is in grafana/grafana, the single source of truth for any translated language is Crowdin
- To update phrases in any translated language, edit the phrase in Crowdin. Do not edit the `{locale}/grafana.json`
## How to add a new translation phrase
@@ -40,7 +41,8 @@ const ErrorMessage = ({ id, message }) => <Trans i18nKey={`errors.${id}`}>There
2. Upon reload, the default English phrase will appear on the page.
3. Before submitting your PR, run the `yarn i18n:extract` command to extract the messages you added into the `grafana.json` file and make them available for translation.
3. Before submitting your PR, run the `yarn i18n:extract` command to extract the messages you added into the `public/locales/en-US/grafana.json` file and make them available for translation.
**Note:** All other languages will receive their translations when they are ready to be downloaded from Crowdin.
### Plain JS usage
@@ -64,20 +66,20 @@ While the `t` function can technically be used outside of React functions (e.g,
## How to add a new language
1. Add new locale in Crowdin and sync files to repo
1. Add new locale in Crowdin and download files to repo
1. Grafana OSS Crowdin project -> "dot dot dot" menu in top right -> Target languages
2. Grafana OSS Crowdin project -> Integrations -> Github -> Sync Now
3. If Crowdin's locale code is different from our IETF language tag, add a custom mapping in Project Settings -> Language mapping
2. Update `public/app/core/internationalization/constants.ts` (add new constant, and add to `LOCALES`)
3. Update `public/locales/i18next-parser.config.js` to add the new locale to `locales`
4. Run `yarn i18n:extract` and commit the result
2. If Crowdin's locale code is different from our IETF language tag, add a custom mapping in Project Settings -> Language mapping
3. GH repo grafana/grafana -> Actions -> Choose `Crowdin Download Action` -> Run workflow -> Creates a PR automatically
2. Review the PR `I18n: Download translations from Crowdin`
3. Update `public/app/core/internationalization/constants.ts` (add new constant, and add to `LOCALES`) and add changes to the open PR
4. Approve and merge the PR
## How translations work in Grafana
Grafana uses the [i18next](https://www.i18next.com/) framework for managing translating phrases in the Grafana frontend. It:
- Marks up phrases within our code for extraction
- Extracts phrases into messages catalogues for translating in external systems
- Extracts phrases into the default messages catalogue for translating in external systems
- Manages the user's locale and putting the translated phrases in the UI
Grafana will load the message catalogue JSON before the initial render.
+3 -4
View File
@@ -2,7 +2,6 @@ files:
- source: /public/locales/en-US/grafana.json
translation: /public/locales/%locale%/%original_file_name%
type: i18next_json
pull_request_title: 'I18n: Crowdin sync'
pull_request_labels:
- area/internationalization
- no-changelog
# The following are pulled from env variables
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_PERSONAL_TOKEN
@@ -26,13 +26,13 @@ In the Contact points tab, you can see a list of your contact points.
**Note:** You can edit an existing contact by clicking the Edit icon.
2. Execute a template from one or more fields such as Message and Subject:
1. Execute a template from one or more fields such as Message and Subject:
{{< figure max-width="940px" src="/static/img/docs/alerting/unified/use-notification-template-9-4.png" caption="Use notification template" >}}
For more information on how to write and execute templates, refer to [Using Go's templating language][using-go-templating-language] and [Create notification templates][create-notification-templates].
3. Click Save template.
1. Click **Save contact point**.
{{% docs/reference %}}
[create-notification-templates]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/alerting/manage-notifications/template-notifications/create-notification-templates"
@@ -2,7 +2,7 @@
aliases:
- ../provision-alerting-resources/
canonical: https://grafana.com/docs/grafana/latest/alerting/set-up/provision-alerting-resources/
description: Provision alerting resources
description: Import and export alerting resources
keywords:
- grafana
- alerting
@@ -14,23 +14,23 @@ labels:
- cloud
- enterprise
- oss
title: Provision Grafana Alerting resources
title: Import and export Grafana Alerting resources
weight: 300
---
# Provision Grafana Alerting resources
# Import and export Grafana Alerting resources
Alerting infrastructure is often complex, with many pieces of the pipeline that often live in different places. Scaling this across multiple teams and organizations is an especially challenging task. Grafana Alerting provisioning makes this process easier by enabling you to create, manage, and maintain your alerting data in a way that best suits your organization.
Alerting infrastructure is often complex, with many pieces of the pipeline that often live in different places. Scaling this across multiple teams and organizations is an especially challenging task. Importing and exporting (or provisioning) your alerting resources in Grafana Alerting makes this process easier by enabling you to create, manage, and maintain your alerting data in a way that best suits your organization.
Provisioning for Grafana Alerting supports alert rules, contact points, notification policies, mute timings, and templates.
You can import alert rules, contact points, notification policies, mute timings, and templates.
You cannot edit provisioned alerting resources in the Grafana UI in the same way as unprovisioned alerting resources. You can only edit provisioned contact points, notification policies, templates, and mute timings in the source where they were created. For example, if you provision your alerting resources using files from disk, you cannot edit the data in Terraform or from within Grafana.
You cannot edit imported alerting resources in the Grafana UI in the same way as alerting resources that were not imported. You can only edit imported contact points, notification policies, templates, and mute timings in the source where they were created. For example, if you manage your alerting resources using files from disk, you cannot edit the data in Terraform or from within Grafana.
To modify provisioned alert rules, you can use the **Modify export** feature to edit and then export.
To modify imported alert rules, you can use the **Modify export** feature to edit and then export.
Choose from the options below to provision your Grafana Alerting resources.
Choose from the options below to import your Grafana Alerting resources.
1. Use file provisioning to provision your Grafana Alerting resources, such as alert rules and contact points, through files on disk.
1. Use file provisioning to manage your Grafana Alerting resources, such as alert rules and contact points, through files on disk.
{{% admonition type="note" %}}
File provisioning is not available in Grafana Cloud instances.
@@ -14,13 +14,13 @@ labels:
- cloud
- enterprise
- oss
title: Create and manage alerting resources using file provisioning
title: Use file provisioning to manage alerting resources
weight: 100
---
## Create and manage alerting resources using file provisioning
## Use file provisioning to manage alerting resources
Provision your alerting resources using files from disk. When you start Grafana, the data from these files is created in your Grafana system. Grafana adds any new resources you created, updates any that you changed, and deletes old ones.
Manage your alerting resources using files from disk. When you start Grafana, the data from these files is created in your Grafana system. Grafana adds any new resources you created, updates any that you changed, and deletes old ones.
Arrange your files in a directory in a way that best suits your use case. For example, you can choose a team-based layout where every team has its own file, you can have one big file for all your teams; or you can have one file per resource type.
@@ -28,9 +28,9 @@ Details on how to set up the files and which fields are required for each object
**Note:**
Provisioning takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API][reload-provisioning-configurations].
Importing takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API][reload-provisioning-configurations].
### Provision alert rules
### Import alert rules
Create or delete alert rules in your Grafana instance(s).
@@ -41,11 +41,11 @@ Create or delete alert rules in your Grafana instance(s).
Example configuration files can be found below.
1. Ensure that your files are in the right directory on the node running the Grafana server, so that they deploy alongside your Grafana instance(s).
1. Delete the alert rules in Grafana that will be provisioned.
1. Delete the alert rules in Grafana that are going to be imported.
**Note:**
If you do not delete the alert rule, it will clash with the provisioned alert rule once uploaded.
If you do not delete the alert rule, it will clash with the imported alert rule once uploaded.
Here is an example of a configuration file for creating alert rules.
@@ -134,7 +134,7 @@ deleteRules:
uid: my_id_1
```
### Provision contact points
### Import contact points
Create or delete contact points in your Grafana instance(s).
@@ -495,7 +495,7 @@ settings:
{{ template "default.title" . }}
```
### Provision notification policies
### Import notification policies
Create or reset the notification policy tree in your Grafana instance(s).
@@ -587,7 +587,7 @@ In Grafana, the entire notification policy tree is considered a single, large re
Since the policy tree is a single resource, applying it will overwrite a policy tree created through any other means.
### Provision templates
### Import templates
Create or delete templates in your Grafana instance(s).
@@ -627,7 +627,7 @@ deleteTemplates:
name: my_first_template
```
### Provision mute timings
### Import mute timings
Create or delete mute timings in your Grafana instance(s).
@@ -13,11 +13,11 @@ labels:
products:
- enterprise
- oss
title: Create and manage alerting resources using Terraform
title: Use Terraform to manage alerting resources
weight: 200
---
# Create and manage alerting resources using Terraform
# Use Terraform to manage alerting resources
Use Terraform’s Grafana Provider to manage your alerting resources and provision them into your Grafana system. Terraform provider support for Grafana Alerting makes it easy to create, manage, and maintain your entire Grafana Alerting stack as code.
@@ -13,12 +13,12 @@ labels:
- cloud
- enterprise
- oss
menuTitle: Manage provisioned resources in Grafana
title: Manage provisioned alerting resources in Grafana
menuTitle: Manage provisioned alerting resources
title: Manage provisioned alerting resources
weight: 300
---
# Manage provisioned alerting resources in Grafana
# Manage provisioned alerting resources
Verify that your alerting resources were created in Grafana, as well as edit or export your provisioned alerting resources.
@@ -58,10 +58,11 @@ title: TestDataDataQuery kind
### NodesQuery
| Property | Type | Required | Default | Description |
|----------|---------|----------|---------|------------------------------------------------------------|
| `count` | integer | No | | |
| `type` | string | No | | Possible values are: `random`, `response`, `random edges`. |
| Property | Type | Required | Default | Description |
|----------|---------|----------|---------|-------------------------------------------------------------------------------------|
| `count` | integer | No | | |
| `seed` | integer | No | | |
| `type` | string | No | | Possible values are: `random`, `response_small`, `response_medium`, `random edges`. |
### PulseWaveQuery
@@ -1,6 +1,7 @@
---
aliases:
- ../basics/timeseries/
- /docs/grafana-cloud/introduction/prometheus/
description: Introduction to Prometheus
keywords:
- grafana
@@ -3,6 +3,7 @@ aliases:
- ../basics/timeseries-dimensions/
- ../getting-started/timeseries-dimensions/
- ../guides/timeseries-dimensions/
- /docs/grafana-cloud/introduction/timeseries-dimensions/
description: time series dimensions
keywords:
- grafana
@@ -1,6 +1,7 @@
---
aliases:
- ../basics/timeseries/
- /docs/grafana-cloud/introduction/timeseries/
description: Introduction to time series
keywords:
- grafana
@@ -38,6 +38,20 @@ For visualizations like stat, gauge, or bar gauge you can click anywhere on the
If there's only one data link in the visualization, clicking anywhere on the visualization opens the link rather than the context menu.
## Supported visualizations
You can configure data links for the following visualizations:
| | | |
| -------------------------- | ---------------------- | -------------------------------- |
| [Bar chart][bar chart] | [Geomap][geomap] | [State timeline][state timeline] |
| [Bar gauge][bar gauge] | [Heatmap][heatmap] | [Status history][status history] |
| [Candlestick][candlestick] | [Histogram][histogram] | [Table][table] |
| [Canvas][canvas] | [Pie chart][pie chart] | [Time series][time series] |
| [Gauge][gauge] | [Stat][stat] | [Trend][trend] |
<!--Also xy chart -->
## Data link variables
Variables in data links let you send people to a detailed dashboard with preserved data filters. For example, you could use variables to specify a label, time range, series, or variable selection.
@@ -136,6 +150,51 @@ If you want to add all of the current dashboard's variables to the URL, then use
1. Click the **Save dashboard** icon to save your changes to the dashboard.
{{% docs/reference %}}
[bar chart]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/bar-chart"
[bar chart]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/bar-chart"
[bar gauge]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/bar-gauge"
[bar gauge]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/bar-gauge"
[candlestick]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/candlestick"
[candlestick]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/candlestick"
[canvas]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/canvas"
[canvas]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/canvas"
[gauge]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/gauge"
[gauge]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/gauge"
[geomap]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/geomap"
[geomap]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/geomap"
[heatmap]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/heatmap"
[heatmap]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/heatmap"
[histogram]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/histogram"
[histogram]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/histogram"
[pie chart]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/pie-chart"
[pie chart]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/pie-chart"
[stat]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/stat"
[stat]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/stat"
[state timeline]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/state-timeline"
[state timeline]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/state-timeline"
[status history]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/status-history"
[status history]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/status-history"
[table]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/table"
[table]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/table"
[time series]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/time-series"
[time series]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/time-series"
[trend]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/trend"
[trend]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/trend"
[Cloudwatch]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/datasources/aws-cloudwatch/query-editor#deep-link-grafana-panels-to-the-cloudwatch-console-1"
[Cloudwatch]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/connect-externally-hosted/data-sources/aws-cloudwatch/query-editor#deep-link-grafana-panels-to-the-cloudwatch-console-1"
@@ -30,6 +30,18 @@ For example, the mapping applied in the following image causes the visualization
Value mappings bypass unit formatting set in the **Standard options** section of panel editor, like color or number of decimal places displayed. When value mappings are present in a panel, Grafana displays a summary of them in the **Value mappings** section of the editor panel.
## Supported visualizations
You can configure value mappings for the following visualizations:
| | | |
| -------------------------- | -------------------------------- | -------------------------------- |
| [Bar chart][bar chart] | [Geomap][geomap] | [Status history][status history] |
| [Bar gauge][bar gauge] | [Histogram][histogram] | [Table][table] |
| [Candlestick][candlestick] | [Pie chart][pie chart] | [Time series][time series] |
| [Canvas][canvas] | [Stat][stat] | [Trend][trend] |
| [Gauge][gauge] | [State timeline][state timeline] | |
## Types of value mappings
Grafana supports the following value mapping types:
@@ -98,6 +110,51 @@ The following image shows a table visualization with value mappings. If you want
1. (Optional) Enter display text.
1. (Optional) Set the color.
1. (Optional) Set an icon (canvas visualizations only).
1. Click **Update** to save the value mapping.
After you've added a mapping, the **Edit value mappings** button replaces the **Add value mappings** button. Click the edit button to add or update mappings.
{{% docs/reference %}}
[bar chart]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/bar-chart"
[bar chart]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/bar-chart"
[bar gauge]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/bar-gauge"
[bar gauge]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/bar-gauge"
[candlestick]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/candlestick"
[candlestick]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/candlestick"
[canvas]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/canvas"
[canvas]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/canvas"
[gauge]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/gauge"
[gauge]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/gauge"
[geomap]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/geomap"
[geomap]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/geomap"
[histogram]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/histogram"
[histogram]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/histogram"
[pie chart]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/pie-chart"
[pie chart]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/pie-chart"
[stat]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/stat"
[stat]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/stat"
[state timeline]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/state-timeline"
[state timeline]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/state-timeline"
[status history]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/status-history"
[status history]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/status-history"
[table]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/table"
[table]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/table"
[time series]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/time-series"
[time series]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/time-series"
[trend]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/trend"
[trend]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/trend"
{{% /docs/reference %}}
@@ -57,7 +57,6 @@ Some features are enabled by default. You can disable these feature by setting t
| `lokiQueryHints` | Enables query hints for Loki | Yes |
| `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | |
| `jitterAlertRules` | Distributes alert rule evaluations more evenly over time, by rule group | |
| `slateAutocomplete` | Adjusts the behaviour of the slate editor to properly handle autocomplete. Feature toggled for safety. | Yes |
## Preview feature toggles
@@ -174,6 +173,7 @@ Experimental features might be changed or removed without prior notice.
| `newFolderPicker` | Enables the nested folder picker without having nested folders enabled |
| `onPremToCloudMigrations` | In-development feature that will allow users to easily migrate their on-prem Grafana instances to Grafana Cloud. |
| `promQLScope` | In-development feature that will allow injection of labels into prometheus queries. |
| `nodeGraphDotLayout` | Changed the layout algorithm for the node graph |
## Development feature toggles
@@ -185,4 +185,5 @@ The following toggles require explicitly setting Grafana's [app mode]({{< relref
| `externalServiceAuth` | Starts an OAuth2 authentication provider for external services |
| `grafanaAPIServerWithExperimentalAPIs` | Register experimental APIs with the k8s API server |
| `grafanaAPIServerEnsureKubectlAccess` | Start an additional https handler and write kubectl options |
| `kubernetesQueryServiceRewrite` | Rewrite requests targeting /ds/query to the query service |
| `panelTitleSearchInV1` | Enable searching for dashboards using panel title in search v1 |
@@ -50,7 +50,7 @@ auto_login = true
## Skip organization role sync
To prevent the sync of org roles from Grafana.com, set `skip_org_role_sync` to `true`. This is useful if you want to manage the organization roles for your users from within Grafana.
If a user signs in with their Grafana.com credentials, their assigned org role overrides the role defined in the Grafana instance. To prevent Grafana.com roles from synchronizing, set `skip_org_role_sync` to `true`. This is useful if you want to manage the organization roles for your users from within Grafana.
```ini
[auth.grafana_com]
+1 -1
View File
@@ -63,7 +63,7 @@ require (
github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code
github.com/grafana/grafana-aws-sdk v0.19.1 // @grafana/aws-datasources
github.com/grafana/grafana-azure-sdk-go v1.11.0 // @grafana/partner-datasources
github.com/grafana/grafana-plugin-sdk-go v0.204.0 // @grafana/plugins-platform-backend
github.com/grafana/grafana-plugin-sdk-go v0.206.0 // @grafana/plugins-platform-backend
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform
github.com/hashicorp/go-hclog v1.6.2 // @grafana/plugins-platform-backend
github.com/hashicorp/go-plugin v1.6.0 // @grafana/plugins-platform-backend
+2 -2
View File
@@ -1832,8 +1832,8 @@ github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ=
github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW39s+c+LetqSCjFj7xxPC5+M=
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
github.com/grafana/grafana-plugin-sdk-go v0.204.0 h1:4+VtZEqmNwfN9OvDersv7rN861tKTvJdaZKCwfuIAmc=
github.com/grafana/grafana-plugin-sdk-go v0.204.0/go.mod h1:MetVbF3bGwmUvxulWsl0SskJFUq3+ZGnyjDH0dQ4sSg=
github.com/grafana/grafana-plugin-sdk-go v0.206.0 h1:DQK2M07/q0t7wqcJBSw6O/FQJZC1tm24uzGfHbA71+A=
github.com/grafana/grafana-plugin-sdk-go v0.206.0/go.mod h1:MetVbF3bGwmUvxulWsl0SskJFUq3+ZGnyjDH0dQ4sSg=
github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 h1:1YNoeIhii4UIIQpCPU+EXidnqf449d0C3ZntAEt4KSo=
github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482/go.mod h1:GNcfpy5+SY6RVbNGQW264gC0r336Dm+0zgQ5vt6+M8Y=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20231027171310-70c52bf65758 h1:ATUhvJSJwzdzhnmzUI92fxVFqyqmcnzJ47wtHTK3LW4=
+3
View File
@@ -29,6 +29,9 @@ if [[ "${UPDATE_API_KNOWN_VIOLATIONS:-}" == "true" ]]; then
fi
for api_pkg in $(ls ./pkg/apis); do
if [[ "${1-}" != "" && ${api_pkg} != $1 ]]; then
continue
fi
for pkg_version in $(ls ./pkg/apis/${api_pkg}); do
echo "Generating openapi package for ${api_pkg}, version=${pkg_version} ..."
grafana::codegen::gen_openapi \
+9 -7
View File
@@ -82,8 +82,8 @@
"@swc/core": "1.3.107",
"@swc/helpers": "0.5.3",
"@testing-library/dom": "9.3.4",
"@testing-library/jest-dom": "6.4.0",
"@testing-library/react": "14.2.0",
"@testing-library/jest-dom": "6.4.1",
"@testing-library/react": "14.2.1",
"@testing-library/user-event": "14.5.2",
"@types/angular": "1.8.9",
"@types/angular-route": "1.7.6",
@@ -101,7 +101,7 @@
"@types/gtag.js": "^0.0.18",
"@types/history": "4.7.11",
"@types/hoist-non-react-statics": "3.3.5",
"@types/jest": "29.5.11",
"@types/jest": "29.5.12",
"@types/jquery": "3.5.29",
"@types/js-yaml": "^4.0.5",
"@types/jsurl": "^1.2.28",
@@ -236,7 +236,7 @@
"@grafana/aws-sdk": "0.3.1",
"@grafana/data": "workspace:*",
"@grafana/e2e-selectors": "workspace:*",
"@grafana/experimental": "1.7.8",
"@grafana/experimental": "1.7.9",
"@grafana/faro-core": "^1.3.6",
"@grafana/faro-web-sdk": "^1.3.6",
"@grafana/flamegraph": "workspace:*",
@@ -245,7 +245,7 @@
"@grafana/monaco-logql": "^0.0.7",
"@grafana/o11y-ds-frontend": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^2.2.0",
"@grafana/scenes": "^2.4.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
@@ -257,6 +257,8 @@
"@locker/near-membrane-dom": "0.13.3",
"@locker/near-membrane-shared": "0.13.3",
"@locker/near-membrane-shared-dom": "0.13.3",
"@msagl/core": "^1.1.16",
"@msagl/parser": "^1.1.16",
"@opentelemetry/api": "1.7.0",
"@opentelemetry/exporter-collector": "0.25.0",
"@opentelemetry/semantic-conventions": "1.21.0",
@@ -297,8 +299,8 @@
"d3": "7.8.5",
"d3-force": "3.0.0",
"d3-scale-chromatic": "3.0.0",
"dangerously-set-html-content": "1.0.9",
"date-fns": "2.30.0",
"dangerously-set-html-content": "1.1.0",
"date-fns": "3.3.1",
"debounce-promise": "3.1.2",
"diff": "^5.1.0",
"emotion": "11.0.0",
+6 -6
View File
@@ -40,8 +40,8 @@
"@types/d3-interpolate": "^3.0.0",
"@types/string-hash": "1.1.3",
"d3-interpolate": "3.0.1",
"date-fns": "2.30.0",
"dompurify": "^2.4.3",
"date-fns": "3.3.1",
"dompurify": "^3.0.0",
"eventemitter3": "5.0.1",
"fast_array_intersect": "1.1.0",
"history": "4.10.1",
@@ -67,12 +67,12 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "15.2.3",
"@testing-library/dom": "9.3.4",
"@testing-library/jest-dom": "6.4.0",
"@testing-library/react": "14.2.0",
"@testing-library/jest-dom": "6.4.1",
"@testing-library/react": "14.2.1",
"@testing-library/user-event": "14.5.2",
"@types/dompurify": "^2",
"@types/dompurify": "^3.0.0",
"@types/history": "4.7.11",
"@types/jest": "29.5.11",
"@types/jest": "29.5.12",
"@types/jquery": "3.5.29",
"@types/lodash": "4.14.202",
"@types/marked": "5.0.2",
@@ -1,6 +1,4 @@
import { Duration, Interval, isAfter } from 'date-fns';
import add from 'date-fns/add';
import intervalToDuration from 'date-fns/intervalToDuration';
import { add, Duration, intervalToDuration, Interval, isAfter } from 'date-fns';
const durationMap: { [key in Required<keyof Duration>]: string[] } = {
years: ['y', 'Y', 'years'],
@@ -204,6 +204,25 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
}
}
// If there is only one row in the data frame, then set the
// valueRowIndex to that one row. This allows the data macros in
// things like links to access other fields from the data frame.
//
// If there were more rows, it still may be sane to set the row
// index, but it may be confusing; the calculation may have
// selected a value from a different row or it may have aggregated
// the values from multiple rows, so to make just the first row
// available would be arbitrary. For now, the users will have to
// ensure that the data frame has just one row if they want data
// link referencing other fields to work.
//
// TODO: A more complete solution here would be to allow the
// calculation to report a relevant row and use that value. For
// example, a common calculation is 'lastNotNull'. It'd be nifty to
// know which row the display value corresponds to in that case if
// there were potentially many
const valueRowIndex = dataFrame.length === 1 ? 0 : undefined;
values.push({
name: calc,
field: config,
@@ -215,6 +234,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
? () =>
fieldLinksSupplier({
calculatedValue: displayValue,
valueRowIndex,
})
: () => [],
hasLinks: hasLinks(field),
@@ -55,19 +55,6 @@ const postProcessTransform =
return after;
}
// Add a key to the metadata if the data changed
for (const series of after) {
if (!series.meta) {
series.meta = {};
}
if (!series.meta.transformations) {
series.meta.transformations = [info.id];
} else {
series.meta.transformations = [...series.meta.transformations, info.id];
}
}
// Add back the filtered out frames
if (matcher) {
// keep the frame order the same
@@ -109,11 +109,6 @@ describe('ensureColumns transformer', () => {
},
],
"length": 2,
"meta": {
"transformations": [
"ensureColumns",
],
},
}
`);
});
@@ -105,6 +105,7 @@ export interface AnalyticsSettings {
export interface CurrentUserDTO {
isSignedIn: boolean;
id: number;
uid: string;
externalUserId: string;
login: string;
email: string;
-3
View File
@@ -54,9 +54,6 @@ export interface QueryResultMeta {
/** Meta Notices */
notices?: QueryResultMetaNotice[];
/** Used to track transformation ids that where part of the processing */
transformations?: string[];
/** Currently used to show results in Explore only in preferred visualisation option */
preferredVisualisationType?: PreferredVisualisationType;
@@ -63,6 +63,7 @@ export interface FeatureToggles {
prometheusMetricEncyclopedia?: boolean;
influxdbBackendMigration?: boolean;
influxqlStreamingParser?: boolean;
influxdbRunQueriesInParallel?: boolean;
clientTokenRotation?: boolean;
prometheusDataplane?: boolean;
lokiMetricDataplane?: boolean;
@@ -130,6 +131,7 @@ export interface FeatureToggles {
transformationsVariableSupport?: boolean;
kubernetesPlaylists?: boolean;
kubernetesSnapshots?: boolean;
kubernetesQueryServiceRewrite?: boolean;
cloudWatchBatchQueries?: boolean;
recoveryThreshold?: boolean;
lokiStructuredMetadata?: boolean;
@@ -174,5 +176,5 @@ export interface FeatureToggles {
onPremToCloudMigrations?: boolean;
alertingSaveStatePeriodic?: boolean;
promQLScope?: boolean;
slateAutocomplete?: boolean;
nodeGraphDotLayout?: boolean;
}
@@ -145,14 +145,14 @@ export const Pages = {
applyButton: 'data-testid Variable editor Apply button',
},
QueryVariable: {
queryOptionsDataSourceSelect: Components.DataSourcePicker.container,
queryOptionsDataSourceSelect: Components.DataSourcePicker.inputV2,
queryOptionsRefreshSelect: 'Variable editor Form Query Refresh select',
queryOptionsRefreshSelectV2: 'data-testid Variable editor Form Query Refresh select',
queryOptionsRegExInput: 'Variable editor Form Query RegEx field',
queryOptionsRegExInputV2: 'data-testid Variable editor Form Query RegEx field',
queryOptionsSortSelect: 'Variable editor Form Query Sort select',
queryOptionsSortSelectV2: 'data-testid Variable editor Form Query Sort select',
queryOptionsQueryInput: 'Variable editor Form Default Variable Query Editor textarea',
queryOptionsQueryInput: 'data-testid Variable editor Form Default Variable Query Editor textarea',
valueGroupsTagsEnabledSwitch: 'Variable editor Form Query UseTags switch',
valueGroupsTagsTagsQueryInput: 'Variable editor Form Query TagsQuery field',
valueGroupsTagsTagsValuesQueryInput: 'Variable editor Form Query TagsValuesQuery field',
+1 -1
View File
@@ -62,7 +62,7 @@
"@grafana/tsconfig": "^1.2.0-rc1",
"@rollup/plugin-node-resolve": "15.2.3",
"@testing-library/jest-dom": "^6.1.2",
"@testing-library/react": "14.2.0",
"@testing-library/react": "14.2.1",
"@testing-library/user-event": "14.5.2",
"@types/d3": "^7",
"@types/jest": "^29.5.4",
@@ -20,7 +20,7 @@
"@emotion/css": "11.11.2",
"@grafana/data": "workspace:*",
"@grafana/e2e-selectors": "workspace:*",
"@grafana/experimental": "1.7.8",
"@grafana/experimental": "1.7.9",
"@grafana/runtime": "workspace:*",
"@grafana/schema": "workspace:*",
"@grafana/ui": "workspace:*",
@@ -32,7 +32,7 @@
"devDependencies": {
"@grafana/tsconfig": "^1.2.0-rc1",
"@testing-library/jest-dom": "^6.1.2",
"@testing-library/react": "14.2.0",
"@testing-library/react": "14.2.1",
"@testing-library/user-event": "14.5.2",
"@types/jest": "^29.5.4",
"@types/react": "18.2.48",
+2 -2
View File
@@ -54,11 +54,11 @@
"@rollup/plugin-commonjs": "25.0.7",
"@rollup/plugin-node-resolve": "15.2.3",
"@testing-library/dom": "9.3.4",
"@testing-library/react": "14.2.0",
"@testing-library/react": "14.2.1",
"@testing-library/user-event": "14.5.2",
"@types/angular": "1.8.9",
"@types/history": "4.7.11",
"@types/jest": "29.5.11",
"@types/jest": "29.5.12",
"@types/lodash": "4.14.202",
"@types/react": "18.2.48",
"@types/react-dom": "18.2.18",
+1 -1
View File
@@ -15,7 +15,7 @@ export {
} from './utils/plugin';
export { reportMetaAnalytics, reportInteraction, reportPageview, reportExperimentView } from './analytics/utils';
export { featureEnabled } from './utils/licensing';
export { logInfo, logDebug, logWarning, logError } from './utils/logging';
export { logInfo, logDebug, logWarning, logError, createMonitoringLogger } from './utils/logging';
export {
DataSourceWithBackend,
HealthCheckError,
+53 -1
View File
@@ -1,4 +1,4 @@
import { faro, LogLevel, LogContext } from '@grafana/faro-web-sdk';
import { faro, LogContext, LogLevel } from '@grafana/faro-web-sdk';
import { config } from '../config';
@@ -57,3 +57,55 @@ export function logError(err: Error, contexts?: LogContext) {
});
}
}
/**
* Creates a monitoring logger with four levels of logging methods: `logDebug`, `logInfo`, `logWarning`, and `logError`.
* These methods use `faro.api.pushX` web SDK methods to report these logs or errors to the Faro collector.
*
* @param {string} source - Identifier for the source of the log messages.
* @param {LogContext} [defaultContext] - Context to be included in every log message.
*
* @returns {Object} Logger object with four methods:
* - `logDebug(message: string, contexts?: LogContext)`: Logs a debug message.
* - `logInfo(message: string, contexts?: LogContext)`: Logs an informational message.
* - `logWarning(message: string, contexts?: LogContext)`: Logs a warning message.
* - `logError(error: Error, contexts?: LogContext)`: Logs an error message.
* Each method combines the `defaultContext` (if provided), the `source`, and an optional `LogContext` parameter into a full context that is included with the log message.
*/
export function createMonitoringLogger(source: string, defaultContext?: LogContext) {
const createFullContext = (contexts?: LogContext) => ({
source: source,
...defaultContext,
...contexts,
});
return {
/**
* Logs a debug message with optional additional context.
* @param {string} message - The debug message to be logged.
* @param {LogContext} [contexts] - Optional additional context to be included.
*/
logDebug: (message: string, contexts?: LogContext) => logDebug(message, createFullContext(contexts)),
/**
* Logs an informational message with optional additional context.
* @param {string} message - The informational message to be logged.
* @param {LogContext} [contexts] - Optional additional context to be included.
*/
logInfo: (message: string, contexts?: LogContext) => logInfo(message, createFullContext(contexts)),
/**
* Logs a warning message with optional additional context.
* @param {string} message - The warning message to be logged.
* @param {LogContext} [contexts] - Optional additional context to be included.
*/
logWarning: (message: string, contexts?: LogContext) => logWarning(message, createFullContext(contexts)),
/**
* Logs an error with optional additional context.
* @param {Error} error - The error object to be logged.
* @param {LogContext} [contexts] - Optional additional context to be included.
*/
logError: (error: Error, contexts?: LogContext) => logError(error, createFullContext(contexts)),
};
}
@@ -75,7 +75,8 @@ export interface SimulationQuery {
export interface NodesQuery {
count?: number;
type?: ('random' | 'response' | 'random edges');
seed?: number;
type?: ('random' | 'response_small' | 'response_medium' | 'random edges');
}
export interface USAQuery {
+2 -2
View File
@@ -16,7 +16,7 @@
"dependencies": {
"@emotion/css": "11.11.2",
"@grafana/data": "workspace:*",
"@grafana/experimental": "1.7.8",
"@grafana/experimental": "1.7.9",
"@grafana/runtime": "workspace:*",
"@grafana/ui": "workspace:*",
"@react-awesome-query-builder/ui": "6.4.2",
@@ -35,7 +35,7 @@
"devDependencies": {
"@grafana/tsconfig": "^1.3.0-rc1",
"@testing-library/jest-dom": "^6.1.2",
"@testing-library/react": "14.2.0",
"@testing-library/react": "14.2.1",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "14.5.2",
"@types/jest": "^29.5.4",
@@ -16,6 +16,9 @@ jest.mock('@grafana/runtime', () => {
},
},
logDebug: jest.fn(),
createMonitoringLogger: jest.fn().mockReturnValue({
logDebug: jest.fn(),
}),
};
});
@@ -1,9 +1,10 @@
import { useEffect } from 'react';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { logDebug, config } from '@grafana/runtime';
import { config } from '@grafana/runtime';
import { SQLOptions } from '../../types';
import { sqlPluginLogger } from '../../utils/logging';
/**
* 1. Moves the database field from the options object to jsonData.database and empties the database field.
@@ -20,7 +21,7 @@ export function useMigrateDatabaseFields<T extends SQLOptions, S = {}>({
// Migrate the database field from the column into the jsonData object
if (options.database) {
logDebug(`Migrating from options.database with value ${options.database} for ${options.name}`);
sqlPluginLogger.logDebug(`Migrating from options.database with value ${options.database} for ${options.name}`);
newOptions.database = '';
newOptions.jsonData = { ...jsonData, database: options.database };
optionsUpdated = true;
@@ -35,7 +36,7 @@ export function useMigrateDatabaseFields<T extends SQLOptions, S = {}>({
) {
const { maxOpenConns, maxIdleConns } = config.sqlConnectionLimits;
logDebug(
sqlPluginLogger.logDebug(
`Setting default max open connections to ${maxOpenConns} and setting max idle connection to ${maxIdleConns}`
);
@@ -0,0 +1,3 @@
import { createMonitoringLogger } from '@grafana/runtime';
export const sqlPluginLogger = createMonitoringLogger('features.plugins.sql');
+4 -4
View File
@@ -65,7 +65,7 @@
"calculate-size": "1.1.1",
"classnames": "2.5.1",
"d3": "7.8.5",
"date-fns": "2.30.0",
"date-fns": "3.3.1",
"hoist-non-react-statics": "3.3.2",
"i18next": "^22.0.0",
"i18next-browser-languagedetector": "^7.0.2",
@@ -129,14 +129,14 @@
"@storybook/react-webpack5": "7.4.5",
"@storybook/theming": "7.4.5",
"@testing-library/dom": "9.3.4",
"@testing-library/jest-dom": "6.4.0",
"@testing-library/react": "14.2.0",
"@testing-library/jest-dom": "6.4.1",
"@testing-library/react": "14.2.1",
"@testing-library/user-event": "14.5.2",
"@types/common-tags": "^1.8.0",
"@types/d3": "7.4.3",
"@types/hoist-non-react-statics": "3.3.5",
"@types/is-hotkey": "0.1.10",
"@types/jest": "29.5.11",
"@types/jest": "29.5.12",
"@types/jquery": "3.5.29",
"@types/lodash": "4.14.202",
"@types/mock-raf": "1.0.6",
@@ -93,7 +93,7 @@ export class Cascader extends PureComponent<CascaderProps, CascaderState> {
for (const option of options) {
const cpy = [...optionPath];
cpy.push(option);
if (!option.items) {
if (!option.items || option.items.length === 0) {
selectOptions.push({
singleLabel: cpy[cpy.length - 1].label,
label: cpy.map((o) => o.label).join(this.props.separator || DEFAULT_SEPARATOR),
@@ -135,23 +135,27 @@ export class Cascader extends PureComponent<CascaderProps, CascaderState> {
: this.props.displayAllSelectedLevels
? selectedOptions.map((option) => option.label).join(this.props.separator || DEFAULT_SEPARATOR)
: selectedOptions[selectedOptions.length - 1].label;
this.setState({
rcValue: value,
const state: CascaderState = {
rcValue: { value, label: activeLabel },
focusCascade: true,
activeLabel,
});
isSearching: false,
};
this.setState(state);
this.props.onSelect(selectedOptions[selectedOptions.length - 1].value);
};
//For select
onSelect = (obj: SelectableValue<string[]>) => {
const valueArray = obj.value || [];
this.setState({
activeLabel: this.props.displayAllSelectedLevels ? obj.label : obj.singleLabel || '',
rcValue: valueArray,
const activeLabel = this.props.displayAllSelectedLevels ? obj.label : obj.singleLabel || '';
const state: CascaderState = {
activeLabel: activeLabel,
rcValue: { value: valueArray, label: activeLabel },
isSearching: false,
});
focusCascade: false,
};
this.setState(state);
this.props.onSelect(valueArray[valueArray.length - 1]);
};
@@ -1,4 +1,4 @@
import formatDuration from 'date-fns/formatDuration';
import { formatDuration } from 'date-fns';
import React, { PureComponent } from 'react';
import { SelectableValue, parseDuration } from '@grafana/data';
@@ -2,52 +2,28 @@ import { getNumCharsToDelete } from './suggestions';
describe('suggestions', () => {
describe('getNumCharsToDelete', () => {
describe('when slateAutocomplete is enabled', () => {
let originalBootData = window.grafanaBootData;
// hacky way to enable the feature toggle for this test
beforeEach(() => {
if (originalBootData) {
window.grafanaBootData = {
...originalBootData,
settings: {
...originalBootData.settings,
featureToggles: {
...originalBootData.settings.featureToggles,
slateAutocomplete: true,
},
},
};
}
});
afterEach(() => {
window.grafanaBootData = originalBootData;
});
const splunkCleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%:\\]/g, '').trim();
it.each([
// | represents the caret position
['$query0 ', '', '', false, 0, undefined, { forward: 0, backward: 0 }], // "|" --> "$query0 |"
['$query0 ', '$que', '$que', false, 0, undefined, { forward: 0, backward: 4 }], // "$que|" --> "$query0 |"
['$query0 ', '$q', '$que', false, 0, undefined, { forward: 2, backward: 2 }], // "$q|ue" --> "$query0 |"
['$query0 ', '$que', '($que)', false, 0, splunkCleanText, { forward: 0, backward: 4 }], // "($que|)" --> "($query0 |)"
['$query0 ', '$que', 'esarvotionUsagePercent=$que', false, 0, undefined, { forward: 0, backward: 4 }], // "esarvotionUsagePercent=$que|" --> "esarvotionUsagePercent=$query0 |"
])(
'should calculate the correct number of characters to delete forwards and backwards',
(suggestionText, typeaheadPrefix, typeaheadText, preserveSuffix, deleteBackwards, cleanText, expected) => {
expect(
getNumCharsToDelete(
suggestionText,
typeaheadPrefix,
typeaheadText,
preserveSuffix,
deleteBackwards,
cleanText
)
).toEqual(expected);
}
);
});
const splunkCleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%:\\]/g, '').trim();
it.each([
// | represents the caret position
['$query0 ', '', '', false, 0, undefined, { forward: 0, backward: 0 }], // "|" --> "$query0 |"
['$query0 ', '$que', '$que', false, 0, undefined, { forward: 0, backward: 4 }], // "$que|" --> "$query0 |"
['$query0 ', '$q', '$que', false, 0, undefined, { forward: 2, backward: 2 }], // "$q|ue" --> "$query0 |"
['$query0 ', '$que', '($que)', false, 0, splunkCleanText, { forward: 0, backward: 4 }], // "($que|)" --> "($query0 |)"
['$query0 ', '$que', 'esarvotionUsagePercent=$que', false, 0, undefined, { forward: 0, backward: 4 }], // "esarvotionUsagePercent=$que|" --> "esarvotionUsagePercent=$query0 |"
])(
'should calculate the correct number of characters to delete forwards and backwards',
(suggestionText, typeaheadPrefix, typeaheadText, preserveSuffix, deleteBackwards, cleanText, expected) => {
expect(
getNumCharsToDelete(
suggestionText,
typeaheadPrefix,
typeaheadText,
preserveSuffix,
deleteBackwards,
cleanText
)
).toEqual(expected);
}
);
});
});
@@ -360,18 +360,10 @@ export function getNumCharsToDelete(
const text = cleanText ? cleanText(typeaheadText) : typeaheadText;
const offset = typeaheadText.indexOf(typeaheadPrefix);
let forward: number;
if (window.grafanaBootData?.settings.featureToggles['slateAutocomplete']) {
const suffixLength =
offset > -1 ? text.length - offset - typeaheadPrefix.length : text.length - typeaheadPrefix.length;
const midWord = Boolean((typeaheadPrefix && suffixLength > 0) || suggestionText === typeaheadText);
forward = midWord && !preserveSuffix ? suffixLength + offset : 0;
} else {
const suffixLength = text.length - typeaheadPrefix.length;
const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText);
forward = midWord && !preserveSuffix ? suffixLength + offset : 0;
}
const suffixLength =
offset > -1 ? text.length - offset - typeaheadPrefix.length : text.length - typeaheadPrefix.length;
const midWord = Boolean((typeaheadPrefix && suffixLength > 0) || suggestionText === typeaheadText);
const forward = midWord && !preserveSuffix ? suffixLength + offset : 0;
return {
forward,
-628
View File
@@ -1,628 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-apiserver/app/aggregator.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: The Kubernetes Authors.
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-apiserver/app/server.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: The Kubernetes Authors.
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/pkg/controlplane/apiserver/apiextensions.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: The Kubernetes Authors.
package aggregator
import (
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"path"
"strings"
"sync"
"time"
servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1"
serviceclientset "github.com/grafana/grafana/pkg/generated/clientset/versioned"
informersv0alpha1 "github.com/grafana/grafana/pkg/generated/informers/externalversions"
"github.com/grafana/grafana/pkg/registry/apis/service"
grafanaAPIServer "github.com/grafana/grafana/pkg/services/grafana-apiserver"
filestorage "github.com/grafana/grafana/pkg/services/grafana-apiserver/storage/file"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
apiextensionsinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
apiextensionsopenapi "k8s.io/apiextensions-apiserver/pkg/generated/openapi"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
genericfeatures "k8s.io/apiserver/pkg/features"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/healthz"
"k8s.io/apiserver/pkg/server/options"
"k8s.io/apiserver/pkg/server/resourceconfig"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/apiserver/pkg/util/openapi"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
v1helper "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helper"
apiregistrationv1beta1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver"
aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"
apiregistrationclientset "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
apiregistrationclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1"
apiregistrationInformers "k8s.io/kube-aggregator/pkg/client/informers/externalversions/apiregistration/v1"
"k8s.io/kube-aggregator/pkg/controllers/autoregister"
apiserver "k8s.io/kube-aggregator/pkg/controllers/status"
aggregatoropenapi "k8s.io/kube-aggregator/pkg/generated/openapi"
"k8s.io/kube-openapi/pkg/common"
)
// AggregatorServerOptions contains the state for the aggregator apiserver
type AggregatorServerOptions struct {
Builders []grafanaAPIServer.APIGroupBuilder
AlternateDNS []string
Config *Config
serviceResolver ServiceResolver
sharedInformerFactory informersv0alpha1.SharedInformerFactory
StdOut io.Writer
StdErr io.Writer
}
func NewAggregatorServerOptions(out, errOut io.Writer,
options *options.RecommendedOptions,
extraConfig *ExtraConfig,
) (*AggregatorServerOptions, error) {
sharedConfig, err := initSharedConfig(options, aggregatorscheme.Codecs, nil)
if err != nil {
klog.Errorf("Error creating shared config: %s", err)
return nil, err
}
sharedInformerFactory, err := initSharedInformerFactory(sharedConfig)
if err != nil {
klog.Errorf("Error creating shared informer factory: %s", err)
return nil, err
}
serviceResolver, err := initServiceResolver(sharedInformerFactory)
if err != nil {
klog.Errorf("Error creating service resolver: %s", err)
return nil, err
}
fakeInformers := informers.NewSharedInformerFactory(fake.NewSimpleClientset(), 10*time.Minute)
builders := []grafanaAPIServer.APIGroupBuilder{
service.NewServiceAPIBuilder(),
}
extensionsConfig, err := initApiExtensionsConfig(options, sharedConfig, fakeInformers, serviceResolver, extraConfig.DataPath)
if err != nil {
klog.Errorf("Error creating extensions config: %s", err)
return nil, err
}
aggregatorConfig, err := initAggregatorConfig(options, sharedConfig, extraConfig, fakeInformers, builders, serviceResolver, extraConfig.DataPath)
if err != nil {
klog.Errorf("Error creating aggregator config: %s", err)
return nil, err
}
return &AggregatorServerOptions{
StdOut: out,
StdErr: errOut,
Builders: builders,
sharedInformerFactory: sharedInformerFactory,
serviceResolver: serviceResolver,
Config: &Config{
Aggregator: aggregatorConfig,
ApiExtensions: extensionsConfig,
SharedConfig: sharedConfig,
extraConfig: extraConfig,
},
}, nil
}
func (o *AggregatorServerOptions) LoadAPIGroupBuilders() error {
// Install schemas
for _, b := range o.Builders {
if err := b.InstallSchema(aggregatorscheme.Scheme); err != nil {
return err
}
}
return nil
}
func initSharedConfig(options *options.RecommendedOptions, codecs serializer.CodecFactory, alternateDNS []string) (*genericapiserver.RecommendedConfig, error) {
if err := options.SecureServing.MaybeDefaultWithSelfSignedCerts(
"localhost", alternateDNS, []net.IP{net.IPv4(127, 0, 0, 1)},
); err != nil {
return nil, fmt.Errorf("error creating self-signed certificates: %v", err)
}
options.Authentication.RemoteKubeConfigFileOptional = true
options.Authorization.RemoteKubeConfigFileOptional = true
options.Admission = nil
if options.CoreAPI.CoreAPIKubeconfigPath == "" {
options.CoreAPI = nil
}
serverConfig := genericapiserver.NewRecommendedConfig(codecs)
// NOTE: AggregatedDiscoveryGroupManager in kube-apiserver is set up by controlplane APIServerConfig creation
// Here, we adopt that one line in addition to what recommendedOptions gives us
// Without it, CRDs work on API routes (and are registered in openapi) but not discoverable by kubectl
serverConfig.AggregatedDiscoveryGroupManager = aggregated.NewResourceManager("apis")
if options.CoreAPI == nil {
if err := modifiedApplyTo(options, serverConfig); err != nil {
return nil, err
}
} else {
if err := options.ApplyTo(serverConfig); err != nil {
return nil, err
}
}
return serverConfig, nil
}
// A copy of ApplyTo in recommended.go, but for >= 0.28, server pkg in apiserver does a bit extra causing
// a panic when CoreAPI is set to nil
func modifiedApplyTo(options *options.RecommendedOptions, config *genericapiserver.RecommendedConfig) error {
if err := options.Etcd.ApplyTo(&config.Config); err != nil {
return err
}
if err := options.EgressSelector.ApplyTo(&config.Config); err != nil {
return err
}
if err := options.Traces.ApplyTo(config.Config.EgressSelector, &config.Config); err != nil {
return err
}
if err := options.SecureServing.ApplyTo(&config.Config.SecureServing, &config.Config.LoopbackClientConfig); err != nil {
return err
}
if err := options.Authentication.ApplyTo(&config.Config.Authentication, config.SecureServing, config.OpenAPIConfig); err != nil {
return err
}
if err := options.Authorization.ApplyTo(&config.Config.Authorization); err != nil {
return err
}
if err := options.Audit.ApplyTo(&config.Config); err != nil {
return err
}
// TODO: determine whether we need flow control (API priority and fairness)
//if err := options.Features.ApplyTo(&config.Config); err != nil {
// return err
//}
if err := options.CoreAPI.ApplyTo(config); err != nil {
return err
}
_, err := options.ExtraAdmissionInitializers(config)
if err != nil {
return err
}
return nil
}
func getMergedOpenAPIDefinitions(builders []grafanaAPIServer.APIGroupBuilder, ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
// Add OpenAPI specs for each group+version
prerequisiteAPIs := grafanaAPIServer.GetOpenAPIDefinitions(builders)(ref)
aggregatorAPIs := aggregatoropenapi.GetOpenAPIDefinitions(ref)
for k, v := range prerequisiteAPIs {
aggregatorAPIs[k] = v
}
return aggregatorAPIs
}
func initSharedInformerFactory(sharedConfig *genericapiserver.RecommendedConfig) (informersv0alpha1.SharedInformerFactory, error) {
serviceClient, err := serviceclientset.NewForConfig(sharedConfig.LoopbackClientConfig)
if err != nil {
return nil, err
}
return informersv0alpha1.NewSharedInformerFactory(
serviceClient,
5*time.Minute, // this is effectively used as a refresh interval right now. Might want to do something nicer later on.
), nil
}
func initServiceResolver(factory informersv0alpha1.SharedInformerFactory) (apiserver.ServiceResolver, error) {
return NewExternalNameResolver(factory.Service().V0alpha1().ExternalNames().Lister()), nil
}
func initApiExtensionsConfig(options *options.RecommendedOptions,
sharedConfig *genericapiserver.RecommendedConfig,
fakeInfomers informers.SharedInformerFactory,
serviceResolver apiserver.ServiceResolver,
dataPath string,
) (*apiextensionsapiserver.Config, error) {
// make a shallow copy to let us twiddle a few things
// most of the config actually remains the same. We only need to mess with a couple items related to the particulars of the api extensions
genericConfig := sharedConfig.Config
genericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}
genericConfig.RESTOptionsGetter = nil
// copy the etcd options so we don't mutate originals.
// we assume that the etcd options have been completed already. avoid messing with anything outside
// of changes to StorageConfig as that may lead to unexpected behavior when the options are applied.
etcdOptions := *options.Etcd
// this is where the true decodable levels come from.
etcdOptions.StorageConfig.Codec = apiextensionsapiserver.Codecs.LegacyCodec(apiextensionsv1beta1.SchemeGroupVersion, v1.SchemeGroupVersion)
// prefer the more compact serialization (v1beta1) for storage until https://issue.k8s.io/82292 is resolved for objects whose v1 serialization is too big but whose v1beta1 serialization can be stored
etcdOptions.StorageConfig.EncodeVersioner = runtime.NewMultiGroupVersioner(apiextensionsv1beta1.SchemeGroupVersion, schema.GroupKind{Group: apiextensionsv1beta1.GroupName})
etcdOptions.SkipHealthEndpoints = true // avoid double wiring of health checks
if err := etcdOptions.ApplyTo(&genericConfig); err != nil {
return nil, err
}
restOptionsGetter := filestorage.NewRESTOptionsGetter(path.Join(dataPath, "grafana-apiextensionsserver"), etcdOptions.StorageConfig)
genericConfig.RESTOptionsGetter = restOptionsGetter
// NOTE: ignoring genericConfig.ResourceTransformers in crdOptionsGetter creation for now
// crdOptionsGetter := apiextensionsoptions.NewCRDRESTOptionsGetter(etcdOptions, genericConfig.ResourceTransformers, )
// The following is equivalent code to apiextensionsoptions.NewCRDRESTOptionsGetter with lesser dependencies
crdEtcdOptions := etcdOptions
crdEtcdOptions.StorageConfig.Codec = unstructured.UnstructuredJSONScheme
crdEtcdOptions.StorageConfig.StorageObjectCountTracker = genericConfig.StorageObjectCountTracker
crdEtcdOptions.WatchCacheSizes = nil // this control is not provided for custom resources
// override MergedResourceConfig with apiextensions defaults and registry
mergedResourceConfig, err := resourceconfig.MergeAPIResourceConfigs(apiextensionsapiserver.DefaultAPIResourceConfigSource(), nil, apiextensionsapiserver.Scheme)
if err != nil {
return nil, err
}
genericConfig.MergedResourceConfig = mergedResourceConfig
genericConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(openapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(apiextensionsopenapi.GetOpenAPIDefinitions), openapinamer.NewDefinitionNamer(apiextensionsapiserver.Scheme, apiextensionsapiserver.Scheme))
apiextensionsConfig := &apiextensionsapiserver.Config{
GenericConfig: &genericapiserver.RecommendedConfig{
Config: genericConfig,
SharedInformerFactory: fakeInfomers,
},
ExtraConfig: apiextensionsapiserver.ExtraConfig{
CRDRESTOptionsGetter: filestorage.NewRESTOptionsGetter(path.Join(dataPath, "grafana-apiextensionsserver"), crdEtcdOptions.StorageConfig),
// TODO: remove the hardcod when HA story is more developed
MasterCount: 1,
// TODO: leaving AuthResolverWrapper unset doesn't impact basic operation of CRDs
// AuthResolverWrapper: authResolverWrapper,
ServiceResolver: serviceResolver,
},
}
// we need to clear the poststarthooks so we don't add them multiple times to all the servers (that fails)
apiextensionsConfig.GenericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}
return apiextensionsConfig, nil
}
func initAggregatorConfig(options *options.RecommendedOptions,
sharedConfig *genericapiserver.RecommendedConfig,
extra *ExtraConfig,
fakeInformers informers.SharedInformerFactory,
builders []grafanaAPIServer.APIGroupBuilder,
serviceResolver apiserver.ServiceResolver,
dataPath string,
) (*aggregatorapiserver.Config, error) {
// make a shallow copy to let us twiddle a few things
// most of the config actually remains the same. We only need to mess with a couple items related to the particulars of the aggregator
genericConfig := sharedConfig.Config
genericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}
genericConfig.RESTOptionsGetter = nil
// prevent generic API server from installing the OpenAPI handler. Aggregator server
// has its own customized OpenAPI handler.
genericConfig.SkipOpenAPIInstallation = true
mergedResourceConfig, err := resourceconfig.MergeAPIResourceConfigs(aggregatorapiserver.DefaultAPIResourceConfigSource(), nil, aggregatorscheme.Scheme)
if err != nil {
return nil, err
}
genericConfig.MergedResourceConfig = mergedResourceConfig
getOpenAPIDefinitionsFunc := func() common.GetOpenAPIDefinitions {
return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return getMergedOpenAPIDefinitions(builders, ref)
}
}
namer := openapinamer.NewDefinitionNamer(aggregatorscheme.Scheme, apiextensionsapiserver.Scheme)
genericConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(getOpenAPIDefinitionsFunc(), namer)
genericConfig.OpenAPIV3Config.Info.Title = "Kubernetes"
genericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(getOpenAPIDefinitionsFunc(), namer)
genericConfig.OpenAPIConfig.Info.Title = "Kubernetes"
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StorageVersionAPI) &&
utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerIdentity) {
// Add StorageVersionPrecondition handler to aggregator-apiserver.
// The handler will block write requests to built-in resources until the
// target resources' storage versions are up-to-date.
genericConfig.BuildHandlerChainFunc = genericapiserver.BuildHandlerChainWithStorageVersionPrecondition
}
// copy the etcd options so we don't mutate originals.
// we assume that the etcd options have been completed already. avoid messing with anything outside
// of changes to StorageConfig as that may lead to unexpected behavior when the options are applied.
etcdOptions := *options.Etcd
etcdOptions.StorageConfig.Codec = aggregatorscheme.Codecs.LegacyCodec(v1.SchemeGroupVersion,
apiregistrationv1beta1.SchemeGroupVersion,
servicev0alpha1.SchemeGroupVersion)
etcdOptions.StorageConfig.EncodeVersioner = runtime.NewMultiGroupVersioner(v1.SchemeGroupVersion,
schema.GroupKind{Group: apiregistrationv1beta1.GroupName},
schema.GroupKind{Group: servicev0alpha1.GROUP})
etcdOptions.SkipHealthEndpoints = true // avoid double wiring of health checks
if err := etcdOptions.ApplyTo(&genericConfig); err != nil {
return nil, err
}
genericConfig.RESTOptionsGetter = filestorage.NewRESTOptionsGetter(path.Join(dataPath, "grafana-aggregator"), etcdOptions.StorageConfig)
genericConfig.DisabledPostStartHooks = genericConfig.DisabledPostStartHooks.Insert("apiservice-status-available-controller")
genericConfig.DisabledPostStartHooks = genericConfig.DisabledPostStartHooks.Insert("start-kube-aggregator-informers")
aggregatorConfig := &aggregatorapiserver.Config{
GenericConfig: &genericapiserver.RecommendedConfig{
Config: genericConfig,
SharedInformerFactory: fakeInformers,
ClientConfig: genericConfig.LoopbackClientConfig,
},
ExtraConfig: aggregatorapiserver.ExtraConfig{
ProxyClientCertFile: extra.ProxyClientCertFile,
ProxyClientKeyFile: extra.ProxyClientKeyFile,
// NOTE: while ProxyTransport can be skipped in the configuration, it allows honoring
// DISABLE_HTTP2, HTTPS_PROXY and NO_PROXY env vars as needed
ProxyTransport: createProxyTransport(),
},
}
aggregatorConfig.ExtraConfig.ServiceResolver = serviceResolver
// we need to clear the poststarthooks so we don't add them multiple times to all the servers (that fails)
aggregatorConfig.GenericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}
return aggregatorConfig, nil
}
func (o *AggregatorServerOptions) CreateAggregatorServer(delegateAPIServer genericapiserver.DelegationTarget, apiExtensionsInformers apiextensionsinformers.SharedInformerFactory) (*aggregatorapiserver.APIAggregator, error) {
completedConfig := o.Config.AggregatorComplete
aggregatorServer, err := completedConfig.NewWithDelegate(delegateAPIServer)
if err != nil {
return nil, err
}
// create controllers for auto-registration
apiRegistrationClient, err := apiregistrationclient.NewForConfig(completedConfig.GenericConfig.LoopbackClientConfig)
if err != nil {
return nil, err
}
autoRegistrationController := autoregister.NewAutoRegisterController(aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(), apiRegistrationClient)
apiServices := apiServicesToRegister(delegateAPIServer, autoRegistrationController)
crdRegistrationController := NewCRDRegistrationController(
apiExtensionsInformers.Apiextensions().V1().CustomResourceDefinitions(),
autoRegistrationController)
// Imbue all builtin group-priorities onto the aggregated discovery
if completedConfig.GenericConfig.AggregatedDiscoveryGroupManager != nil {
for gv, entry := range apiVersionPriorities {
completedConfig.GenericConfig.AggregatedDiscoveryGroupManager.SetGroupVersionPriority(metav1.GroupVersion(gv), int(entry.group), int(entry.version))
}
}
err = aggregatorServer.GenericAPIServer.AddPostStartHook("kube-apiserver-autoregistration", func(context genericapiserver.PostStartHookContext) error {
go crdRegistrationController.Run(5, context.StopCh)
go func() {
crdRegistrationController.WaitForInitialSync()
autoRegistrationController.Run(5, context.StopCh)
}()
return nil
})
if err != nil {
return nil, err
}
err = aggregatorServer.GenericAPIServer.AddBootSequenceHealthChecks(
makeAPIServiceAvailableHealthCheck(
"autoregister-completion",
apiServices,
aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(),
),
)
if err != nil {
return nil, err
}
apiregistrationClient, err := apiregistrationclientset.NewForConfig(completedConfig.GenericConfig.LoopbackClientConfig)
if err != nil {
return nil, err
}
availableController, err := NewAvailableConditionController(
aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(),
o.sharedInformerFactory.Service().V0alpha1().ExternalNames(),
apiregistrationClient.ApiregistrationV1(),
nil,
(func() ([]byte, []byte))(nil),
completedConfig.ExtraConfig.ServiceResolver,
)
if err != nil {
return nil, err
}
aggregatorServer.GenericAPIServer.AddPostStartHookOrDie("apiservice-status-override-available-controller", func(context genericapiserver.PostStartHookContext) error {
// if we end up blocking for long periods of time, we may need to increase workers.
go availableController.Run(5, context.StopCh)
return nil
})
aggregatorServer.GenericAPIServer.AddPostStartHookOrDie("start-grafana-aggregator-informers", func(context genericapiserver.PostStartHookContext) error {
o.sharedInformerFactory.Start(context.StopCh)
aggregatorServer.APIRegistrationInformers.Start(context.StopCh)
return nil
})
// Install the API Group+version
for _, b := range o.Builders {
g, err := b.GetAPIGroupInfo(aggregatorscheme.Scheme, aggregatorscheme.Codecs, completedConfig.GenericConfig.RESTOptionsGetter)
if err != nil {
return nil, err
}
if g == nil || len(g.PrioritizedVersions) < 1 {
continue
}
err = aggregatorServer.GenericAPIServer.InstallAPIGroup(g)
if err != nil {
return nil, err
}
}
return aggregatorServer, nil
}
func makeAPIService(gv schema.GroupVersion) *v1.APIService {
apiServicePriority, ok := apiVersionPriorities[gv]
if !ok {
// if we aren't found, then we shouldn't register ourselves because it could result in a CRD group version
// being permanently stuck in the APIServices list.
klog.Infof("Skipping APIService creation for %v", gv)
return nil
}
return &v1.APIService{
ObjectMeta: metav1.ObjectMeta{Name: gv.Version + "." + gv.Group},
Spec: v1.APIServiceSpec{
Group: gv.Group,
Version: gv.Version,
GroupPriorityMinimum: apiServicePriority.group,
VersionPriority: apiServicePriority.version,
},
}
}
// makeAPIServiceAvailableHealthCheck returns a healthz check that returns healthy
// once all of the specified services have been observed to be available at least once.
func makeAPIServiceAvailableHealthCheck(name string, apiServices []*v1.APIService, apiServiceInformer apiregistrationInformers.APIServiceInformer) healthz.HealthChecker {
// Track the auto-registered API services that have not been observed to be available yet
pendingServiceNamesLock := &sync.RWMutex{}
pendingServiceNames := sets.NewString()
for _, service := range apiServices {
pendingServiceNames.Insert(service.Name)
}
// When an APIService in the list is seen as available, remove it from the pending list
handleAPIServiceChange := func(service *v1.APIService) {
pendingServiceNamesLock.Lock()
defer pendingServiceNamesLock.Unlock()
if !pendingServiceNames.Has(service.Name) {
return
}
if v1helper.IsAPIServiceConditionTrue(service, v1.Available) {
pendingServiceNames.Delete(service.Name)
}
}
// Watch add/update events for APIServices
_, _ = apiServiceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { handleAPIServiceChange(obj.(*v1.APIService)) },
UpdateFunc: func(old, new interface{}) { handleAPIServiceChange(new.(*v1.APIService)) },
})
// Don't return healthy until the pending list is empty
return healthz.NamedCheck(name, func(r *http.Request) error {
pendingServiceNamesLock.RLock()
defer pendingServiceNamesLock.RUnlock()
if pendingServiceNames.Len() > 0 {
return fmt.Errorf("missing APIService: %v", pendingServiceNames.List())
}
return nil
})
}
// priority defines group priority that is used in discovery. This controls
// group position in the kubectl output.
type priority struct {
// group indicates the order of the group relative to other groups.
group int32
// version indicates the relative order of the version inside of its group.
version int32
}
// The proper way to resolve this letting the aggregator know the desired group and version-within-group order of the underlying servers
// is to refactor the genericapiserver.DelegationTarget to include a list of priorities based on which APIs were installed.
// This requires the APIGroupInfo struct to evolve and include the concept of priorities and to avoid mistakes, the core storage map there needs to be updated.
// That ripples out every bit as far as you'd expect, so for 1.7 we'll include the list here instead of being built up during storage.
var apiVersionPriorities = map[schema.GroupVersion]priority{
{Group: "", Version: "v1"}: {group: 18000, version: 1},
// to my knowledge, nothing below here collides
{Group: "admissionregistration.k8s.io", Version: "v1"}: {group: 16700, version: 15},
{Group: "admissionregistration.k8s.io", Version: "v1beta1"}: {group: 16700, version: 12},
{Group: "admissionregistration.k8s.io", Version: "v1alpha1"}: {group: 16700, version: 9},
{Group: "apiextensions.k8s.io", Version: "v1"}: {group: 16700, version: 15},
// Append a new group to the end of the list if unsure.
// You can use min(existing group)-100 as the initial value for a group.
// Version can be set to 9 (to have space around) for a new group.
}
func apiServicesToRegister(delegateAPIServer genericapiserver.DelegationTarget, registration autoregister.AutoAPIServiceRegistration) []*v1.APIService {
apiServices := []*v1.APIService{}
for _, curr := range delegateAPIServer.ListedPaths() {
if curr == "/api/v1" {
apiService := makeAPIService(schema.GroupVersion{Group: "", Version: "v1"})
registration.AddAPIServiceToSyncOnStart(apiService)
apiServices = append(apiServices, apiService)
continue
}
if !strings.HasPrefix(curr, "/apis/") {
continue
}
// this comes back in a list that looks like /apis/rbac.authorization.k8s.io/v1alpha1
tokens := strings.Split(curr, "/")
if len(tokens) != 4 {
continue
}
apiService := makeAPIService(schema.GroupVersion{Group: tokens[2], Version: tokens[3]})
if apiService == nil {
continue
}
registration.AddAPIServiceToSyncOnStart(apiService)
apiServices = append(apiServices, apiService)
}
return apiServices
}
// NOTE: below function imported from https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-apiserver/app/server.go#L197
// createProxyTransport creates the dialer infrastructure to connect to the api servers.
func createProxyTransport() *http.Transport {
// NOTE: We don't set proxyDialerFn but the below SetTransportDefaults will
// See https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/util/net/http.go#L109
var proxyDialerFn utilnet.DialFunc
// Proxying to services is IP-based... don't expect to be able to verify the hostname
proxyTLSClientConfig := &tls.Config{InsecureSkipVerify: true}
proxyTransport := utilnet.SetTransportDefaults(&http.Transport{
DialContext: proxyDialerFn,
TLSClientConfig: proxyTLSClientConfig,
})
return proxyTransport
}
-57
View File
@@ -1,57 +0,0 @@
package aggregator
import (
"github.com/spf13/pflag"
apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/options"
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver"
)
type ExtraConfig struct {
ProxyClientCertFile string
ProxyClientKeyFile string
DataPath string
}
type Config struct {
Aggregator *aggregatorapiserver.Config
ApiExtensions *apiextensionsapiserver.Config
AggregatorComplete aggregatorapiserver.CompletedConfig
ApiExtensionsComplete apiextensionsapiserver.CompletedConfig
recommendedOptions *options.RecommendedOptions
SharedConfig *genericapiserver.RecommendedConfig
extraConfig *ExtraConfig
}
func (c *Config) AddFlags(fs *pflag.FlagSet) {
if c == nil {
return
}
c.recommendedOptions.AddFlags(fs)
}
func (c *Config) Complete() {
if c == nil {
return
}
c.ApiExtensionsComplete = c.ApiExtensions.Complete()
c.AggregatorComplete = c.Aggregator.Complete()
}
func (ec *ExtraConfig) AddFlags(fs *pflag.FlagSet) {
if ec == nil {
return
}
fs.StringVar(&ec.ProxyClientCertFile, "proxy-client-cert-file", ec.ProxyClientCertFile,
"path to proxy client cert file")
fs.StringVar(&ec.ProxyClientKeyFile, "proxy-client-key-file", ec.ProxyClientKeyFile,
"path to proxy client cert file")
}
-214
View File
@@ -1,214 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/pkg/controlplane/controller/crdregistration/crdregistration_controller.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: The Kubernetes Authors.
package aggregator
import (
"fmt"
"time"
"k8s.io/klog/v2"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
crdinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1"
crdlisters "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
)
// AutoAPIServiceRegistration is an interface which callers can re-declare locally and properly cast to for
// adding and removing APIServices
type AutoAPIServiceRegistration interface {
// AddAPIServiceToSync adds an API service to auto-register.
AddAPIServiceToSync(in *v1.APIService)
// RemoveAPIServiceToSync removes an API service to auto-register.
RemoveAPIServiceToSync(name string)
}
type crdRegistrationController struct {
crdLister crdlisters.CustomResourceDefinitionLister
crdSynced cache.InformerSynced
apiServiceRegistration AutoAPIServiceRegistration
syncHandler func(groupVersion schema.GroupVersion) error
syncedInitialSet chan struct{}
// queue is where incoming work is placed to de-dup and to allow "easy" rate limited requeues on errors
// this is actually keyed by a groupVersion
queue workqueue.RateLimitingInterface
}
// NewCRDRegistrationController returns a controller which will register CRD GroupVersions with the auto APIService registration
// controller so they automatically stay in sync.
func NewCRDRegistrationController(crdinformer crdinformers.CustomResourceDefinitionInformer, apiServiceRegistration AutoAPIServiceRegistration) *crdRegistrationController {
c := &crdRegistrationController{
crdLister: crdinformer.Lister(),
crdSynced: crdinformer.Informer().HasSynced,
apiServiceRegistration: apiServiceRegistration,
syncedInitialSet: make(chan struct{}),
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "crd_autoregistration_controller"),
}
c.syncHandler = c.handleVersionUpdate
_, _ = crdinformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
cast := obj.(*apiextensionsv1.CustomResourceDefinition)
c.enqueueCRD(cast)
},
UpdateFunc: func(oldObj, newObj interface{}) {
// Enqueue both old and new object to make sure we remove and add appropriate API services.
// The working queue will resolve any duplicates and only changes will stay in the queue.
c.enqueueCRD(oldObj.(*apiextensionsv1.CustomResourceDefinition))
c.enqueueCRD(newObj.(*apiextensionsv1.CustomResourceDefinition))
},
DeleteFunc: func(obj interface{}) {
cast, ok := obj.(*apiextensionsv1.CustomResourceDefinition)
if !ok {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
klog.V(2).Infof("Couldn't get object from tombstone %#v", obj)
return
}
cast, ok = tombstone.Obj.(*apiextensionsv1.CustomResourceDefinition)
if !ok {
klog.V(2).Infof("Tombstone contained unexpected object: %#v", obj)
return
}
}
c.enqueueCRD(cast)
},
})
return c
}
func (c *crdRegistrationController) Run(workers int, stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
// make sure the work queue is shutdown which will trigger workers to end
defer c.queue.ShutDown()
klog.Infof("Starting crd-autoregister controller")
defer klog.Infof("Shutting down crd-autoregister controller")
// wait for your secondary caches to fill before starting your work
if !cache.WaitForNamedCacheSync("crd-autoregister", stopCh, c.crdSynced) {
return
}
// process each item in the list once
if crds, err := c.crdLister.List(labels.Everything()); err != nil {
utilruntime.HandleError(err)
} else {
for _, crd := range crds {
for _, version := range crd.Spec.Versions {
if err := c.syncHandler(schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name}); err != nil {
utilruntime.HandleError(err)
}
}
}
}
close(c.syncedInitialSet)
// start up your worker threads based on workers. Some controllers have multiple kinds of workers
for i := 0; i < workers; i++ {
// runWorker will loop until "something bad" happens. The .Until will then rekick the worker
// after one second
go wait.Until(c.runWorker, time.Second, stopCh)
}
// wait until we're told to stop
<-stopCh
}
// WaitForInitialSync blocks until the initial set of CRD resources has been processed
func (c *crdRegistrationController) WaitForInitialSync() {
<-c.syncedInitialSet
}
func (c *crdRegistrationController) runWorker() {
// hot loop until we're told to stop. processNextWorkItem will automatically wait until there's work
// available, so we don't worry about secondary waits
for c.processNextWorkItem() {
}
}
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
func (c *crdRegistrationController) processNextWorkItem() bool {
// pull the next work item from queue. It should be a key we use to lookup something in a cache
key, quit := c.queue.Get()
if quit {
return false
}
// you always have to indicate to the queue that you've completed a piece of work
defer c.queue.Done(key)
// do your work on the key. This method will contains your "do stuff" logic
err := c.syncHandler(key.(schema.GroupVersion))
if err == nil {
// if you had no error, tell the queue to stop tracking history for your key. This will
// reset things like failure counts for per-item rate limiting
c.queue.Forget(key)
return true
}
// there was a failure so be sure to report it. This method allows for pluggable error handling
// which can be used for things like cluster-monitoring
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", key, err))
// since we failed, we should requeue the item to work on later. This method will add a backoff
// to avoid hotlooping on particular items (they're probably still not going to work right away)
// and overall controller protection (everything I've done is broken, this controller needs to
// calm down or it can starve other useful work) cases.
c.queue.AddRateLimited(key)
return true
}
func (c *crdRegistrationController) enqueueCRD(crd *apiextensionsv1.CustomResourceDefinition) {
for _, version := range crd.Spec.Versions {
c.queue.Add(schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name})
}
}
func (c *crdRegistrationController) handleVersionUpdate(groupVersion schema.GroupVersion) error {
apiServiceName := groupVersion.Version + "." + groupVersion.Group
// check all CRDs. There shouldn't that many, but if we have problems later we can index them
crds, err := c.crdLister.List(labels.Everything())
if err != nil {
return err
}
for _, crd := range crds {
if crd.Spec.Group != groupVersion.Group {
continue
}
for _, version := range crd.Spec.Versions {
if version.Name != groupVersion.Version || !version.Served {
continue
}
c.apiServiceRegistration.AddAPIServiceToSync(&v1.APIService{
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
Spec: v1.APIServiceSpec{
Group: groupVersion.Group,
Version: groupVersion.Version,
GroupPriorityMinimum: 1000, // CRDs should have relatively low priority
VersionPriority: 100, // CRDs will be sorted by kube-like versions like any other APIService with the same VersionPriority
},
})
return nil
}
}
c.apiServiceRegistration.RemoveAPIServiceToSync(apiServiceName)
return nil
}
+1 -12
View File
@@ -31,7 +31,6 @@ package api
import (
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
@@ -49,8 +48,6 @@ import (
"github.com/grafana/grafana/pkg/services/user"
)
var plog = log.New("api")
// registerRoutes registers all API HTTP routes.
func (hs *HTTPServer) registerRoutes() {
reqNoAuth := middleware.NoAuth()
@@ -418,14 +415,6 @@ func (hs *HTTPServer) registerRoutes() {
pluginRoute.Get("/:pluginId/metrics", reqOrgAdmin, routing.Wrap(hs.CollectPluginMetrics))
})
if hs.Features.IsEnabledGlobally(featuremgmt.FlagFeatureToggleAdminPage) {
apiRoute.Group("/featuremgmt", func(featuremgmtRoute routing.RouteRegister) {
featuremgmtRoute.Get("/state", authorize(ac.EvalPermission(ac.ActionFeatureManagementRead)), hs.GetFeatureMgmtState)
featuremgmtRoute.Get("/", authorize(ac.EvalPermission(ac.ActionFeatureManagementRead)), hs.GetFeatureToggles)
featuremgmtRoute.Post("/", authorize(ac.EvalPermission(ac.ActionFeatureManagementWrite)), hs.UpdateFeatureToggle)
})
}
apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings)
apiRoute.Get("/frontend/assets", hs.GetFrontendAssets)
@@ -515,7 +504,7 @@ func (hs *HTTPServer) registerRoutes() {
// metrics
// DataSource w/ expressions
apiRoute.Post("/ds/query", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(datasources.ActionQuery)), routing.Wrap(hs.QueryMetricsV2))
apiRoute.Post("/ds/query", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(datasources.ActionQuery)), hs.getDSQueryEndpoint())
apiRoute.Group("/alerts", func(alertsRoute routing.RouteRegister) {
alertsRoute.Post("/test", routing.Wrap(hs.AlertTest))
+17 -200
View File
@@ -1,32 +1,22 @@
package api
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/components/simplejson"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
var client = &http.Client{
Timeout: time.Second * 5,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
// swagger:route GET /snapshot/shared-options snapshots getSharingOptions
//
// Get snapshot sharing settings.
@@ -43,58 +33,6 @@ func (hs *HTTPServer) GetSharingOptions(c *contextmodel.ReqContext) {
})
}
type CreateExternalSnapshotResponse struct {
Key string `json:"key"`
DeleteKey string `json:"deleteKey"`
Url string `json:"url"`
DeleteUrl string `json:"deleteUrl"`
}
func createExternalDashboardSnapshot(cmd dashboardsnapshots.CreateDashboardSnapshotCommand, externalSnapshotUrl string) (*CreateExternalSnapshotResponse, error) {
var createSnapshotResponse CreateExternalSnapshotResponse
message := map[string]any{
"name": cmd.Name,
"expires": cmd.Expires,
"dashboard": cmd.Dashboard,
"key": cmd.Key,
"deleteKey": cmd.DeleteKey,
}
messageBytes, err := simplejson.NewFromAny(message).Encode()
if err != nil {
return nil, err
}
resp, err := client.Post(externalSnapshotUrl+"/api/snapshots", "application/json", bytes.NewBuffer(messageBytes))
if err != nil {
return nil, err
}
defer func() {
if err := resp.Body.Close(); err != nil {
plog.Warn("Failed to close response body", "err", err)
}
}()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("create external snapshot response status code %d", resp.StatusCode)
}
if err := json.NewDecoder(resp.Body).Decode(&createSnapshotResponse); err != nil {
return nil, err
}
return &createSnapshotResponse, nil
}
func createOriginalDashboardURL(cmd *dashboardsnapshots.CreateDashboardSnapshotCommand) (string, error) {
dashUID := cmd.Dashboard.Get("uid").MustString("")
if ok := util.IsValidShortUID(dashUID); !ok {
return "", fmt.Errorf("invalid dashboard UID")
}
return fmt.Sprintf("/d/%v", dashUID), nil
}
// swagger:route POST /snapshots snapshots createDashboardSnapshot
//
// When creating a snapshot using the API, you have to provide the full dashboard payload including the snapshot data. This endpoint is designed for the Grafana UI.
@@ -106,95 +44,13 @@ func createOriginalDashboardURL(cmd *dashboardsnapshots.CreateDashboardSnapshotC
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) CreateDashboardSnapshot(c *contextmodel.ReqContext) response.Response {
if !hs.Cfg.SnapshotEnabled {
c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil)
return nil
}
cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
if cmd.Name == "" {
cmd.Name = "Unnamed snapshot"
}
userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID())
if err != nil {
return response.Error(http.StatusInternalServerError,
"Failed to create external snapshot", err)
}
var snapshotUrl string
cmd.ExternalURL = ""
cmd.OrgID = c.SignedInUser.GetOrgID()
cmd.UserID = userID
originalDashboardURL, err := createOriginalDashboardURL(&cmd)
if err != nil {
return response.Error(http.StatusInternalServerError, "Invalid app URL", err)
}
if cmd.External {
if !hs.Cfg.ExternalEnabled {
c.JsonApiErr(http.StatusForbidden, "External dashboard creation is disabled", nil)
return nil
}
resp, err := createExternalDashboardSnapshot(cmd, hs.Cfg.ExternalSnapshotUrl)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Failed to create external snapshot", err)
return nil
}
snapshotUrl = resp.Url
cmd.Key = resp.Key
cmd.DeleteKey = resp.DeleteKey
cmd.ExternalURL = resp.Url
cmd.ExternalDeleteURL = resp.DeleteUrl
cmd.Dashboard = simplejson.New()
metrics.MApiDashboardSnapshotExternal.Inc()
} else {
cmd.Dashboard.SetPath([]string{"snapshot", "originalUrl"}, originalDashboardURL)
if cmd.Key == "" {
var err error
cmd.Key, err = util.GetRandomString(32)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Could not generate random string", err)
return nil
}
}
if cmd.DeleteKey == "" {
var err error
cmd.DeleteKey, err = util.GetRandomString(32)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Could not generate random string", err)
return nil
}
}
snapshotUrl = setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key)
metrics.MApiDashboardSnapshotCreate.Inc()
}
result, err := hs.dashboardsnapshotsService.CreateDashboardSnapshot(c.Req.Context(), &cmd)
if err != nil {
c.JsonApiErr(http.StatusInternalServerError, "Failed to create snapshot", err)
return nil
}
c.JSON(http.StatusOK, util.DynMap{
"key": cmd.Key,
"deleteKey": cmd.DeleteKey,
"url": snapshotUrl,
"deleteUrl": setting.ToAbsUrl("api/snapshots-delete/" + cmd.DeleteKey),
"id": result.ID,
})
return nil
func (hs *HTTPServer) CreateDashboardSnapshot(c *contextmodel.ReqContext) {
dashboardsnapshots.CreateDashboardSnapshot(c, dashboardsnapshot.SnapshotSharingOptions{
SnapshotsEnabled: hs.Cfg.SnapshotEnabled,
ExternalEnabled: hs.Cfg.ExternalEnabled,
ExternalSnapshotName: hs.Cfg.ExternalSnapshotName,
ExternalSnapshotURL: hs.Cfg.ExternalSnapshotUrl,
}, hs.dashboardsnapshotsService)
}
// GET /api/snapshots/:key
@@ -247,38 +103,6 @@ func (hs *HTTPServer) GetDashboardSnapshot(c *contextmodel.ReqContext) response.
return response.JSON(http.StatusOK, dto).SetHeader("Cache-Control", "public, max-age=3600")
}
func deleteExternalDashboardSnapshot(externalUrl string) error {
resp, err := client.Get(externalUrl)
if err != nil {
return err
}
defer func() {
if err := resp.Body.Close(); err != nil {
plog.Warn("Failed to close response body", "err", err)
}
}()
if resp.StatusCode == 200 {
return nil
}
// Gracefully ignore "snapshot not found" errors as they could have already
// been removed either via the cleanup script or by request.
if resp.StatusCode == 500 {
var respJson map[string]any
if err := json.NewDecoder(resp.Body).Decode(&respJson); err != nil {
return err
}
if respJson["message"] == "Failed to get dashboard snapshot" {
return nil
}
}
return fmt.Errorf("unexpected response when deleting external snapshot, status code: %d", resp.StatusCode)
}
// swagger:route GET /snapshots-delete/{deleteKey} snapshots deleteDashboardSnapshotByDeleteKey
//
// Delete Snapshot by deleteKey.
@@ -302,28 +126,16 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *contextmodel.ReqCont
return response.Error(404, "Snapshot not found", nil)
}
query := &dashboardsnapshots.GetDashboardSnapshotQuery{DeleteKey: key}
queryResult, err := hs.dashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
err := dashboardsnapshots.DeleteWithKey(c.Req.Context(), key, hs.dashboardsnapshotsService)
if err != nil {
return response.Err(err)
}
if queryResult.External {
err := deleteExternalDashboardSnapshot(queryResult.ExternalDeleteURL)
if err != nil {
return response.Error(500, "Failed to delete external dashboard", err)
if errors.Is(err, dashboardsnapshots.ErrBaseNotFound) {
return response.Error(404, "Snapshot not found", err)
}
}
cmd := &dashboardsnapshots.DeleteDashboardSnapshotCommand{DeleteKey: queryResult.DeleteKey}
if err := hs.dashboardsnapshotsService.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil {
return response.Error(500, "Failed to delete dashboard snapshot", err)
}
return response.JSON(http.StatusOK, util.DynMap{
"message": "Snapshot deleted. It might take an hour before it's cleared from any CDN caches.",
"id": queryResult.ID,
})
}
@@ -357,8 +169,13 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *contextmodel.ReqContext) respon
return response.Error(http.StatusNotFound, "Failed to get dashboard snapshot", nil)
}
// TODO: enforce org ID same
// if queryResult.OrgID != c.OrgID {
// return response.Error(http.StatusUnauthorized, "OrgID mismatch", nil)
// }
if queryResult.External {
err := deleteExternalDashboardSnapshot(queryResult.ExternalDeleteURL)
err := dashboardsnapshots.DeleteExternalDashboardSnapshot(queryResult.ExternalDeleteURL)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to delete external dashboard", err)
}
+11 -10
View File
@@ -9,13 +9,14 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/web/webtest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
@@ -148,12 +149,11 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey
sc.fakeReqWithParams("GET", sc.url, map[string]string{"deleteKey": "12345"}).exec()
require.Equal(t, 200, sc.resp.Code)
require.Equal(t, 200, sc.resp.Code, "BODY: "+sc.resp.Body.String())
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
require.NoError(t, err)
assert.True(t, strings.HasPrefix(respJSON.Get("message").MustString(), "Snapshot deleted"))
assert.Equal(t, 1, respJSON.Get("id").MustInt())
assert.Equal(t, http.MethodGet, externalRequest.Method)
assert.Equal(t, ts.URL, fmt.Sprintf("http://%s", externalRequest.Host))
@@ -271,7 +271,7 @@ func TestGetDashboardSnapshotNotFound(t *testing.T) {
sc.handlerFunc = hs.DeleteDashboardSnapshot
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
assert.Equal(t, http.StatusNotFound, sc.resp.Code)
assert.Equal(t, http.StatusNotFound, sc.resp.Code, "BODY: "+sc.resp.Body.String())
}, sqlmock)
loggedInUserScenarioWithRole(t,
@@ -282,7 +282,7 @@ func TestGetDashboardSnapshotNotFound(t *testing.T) {
sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"deleteKey": "12345"}).exec()
assert.Equal(t, http.StatusNotFound, sc.resp.Code)
assert.Equal(t, http.StatusNotFound, sc.resp.Code, "BODY: "+sc.resp.Body.String())
}, sqlmock)
}
@@ -345,7 +345,7 @@ func TestGetDashboardSnapshotFailure(t *testing.T) {
sc.handlerFunc = hs.DeleteDashboardSnapshot
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
assert.Equal(t, http.StatusForbidden, sc.resp.Code)
assert.Equal(t, http.StatusForbidden, sc.resp.Code, "BODY: "+sc.resp.Body.String())
}, sqlmock)
loggedInUserScenarioWithRole(t,
@@ -356,7 +356,7 @@ func TestGetDashboardSnapshotFailure(t *testing.T) {
sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"deleteKey": "12345"}).exec()
assert.Equal(t, http.StatusInternalServerError, sc.resp.Code)
assert.Equal(t, http.StatusInternalServerError, sc.resp.Code, "BODY: "+sc.resp.Body.String())
}, sqlmock)
loggedInUserScenarioWithRole(t,
@@ -367,7 +367,7 @@ func TestGetDashboardSnapshotFailure(t *testing.T) {
sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"deleteKey": "12345"}).exec()
assert.Equal(t, http.StatusForbidden, sc.resp.Code)
assert.Equal(t, http.StatusForbidden, sc.resp.Code, "BODY: "+sc.resp.Body.String())
}, sqlmock)
}
@@ -391,6 +391,7 @@ func setUpSnapshotTest(t *testing.T, userId int64, deleteUrl string) dashboardsn
res := &dashboardsnapshots.DashboardSnapshot{
ID: 1,
OrgID: 1,
Key: "12345",
DeleteKey: "54321",
Dashboard: jsonModel,
+1
View File
@@ -30,6 +30,7 @@ type LoginCommand struct {
type CurrentUser struct {
IsSignedIn bool `json:"isSignedIn"`
Id int64 `json:"id"`
UID string `json:"uid"`
Login string `json:"login"`
Email string `json:"email"`
Name string `json:"name"`
-132
View File
@@ -1,132 +0,0 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strconv"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
func (hs *HTTPServer) GetFeatureToggles(ctx *contextmodel.ReqContext) response.Response {
// object being returned
dtos := make([]featuremgmt.FeatureToggleDTO, 0)
// loop through features an add features that should be visible to dtos
for _, ft := range hs.featureManager.GetFlags() {
flag := ft.Name
if hs.featureManager.IsHiddenFromAdminPage(flag, false) {
continue
}
dto := featuremgmt.FeatureToggleDTO{
Name: flag,
Description: ft.Description,
Enabled: hs.featureManager.IsEnabled(ctx.Req.Context(), flag),
ReadOnly: !hs.featureManager.IsEditableFromAdminPage(flag),
}
dtos = append(dtos, dto)
sort.Slice(dtos, func(i, j int) bool {
return dtos[i].Name < dtos[j].Name
})
}
return response.JSON(http.StatusOK, dtos)
}
func (hs *HTTPServer) UpdateFeatureToggle(ctx *contextmodel.ReqContext) response.Response {
featureMgmtCfg := hs.featureManager.Settings
if !featureMgmtCfg.AllowEditing {
return response.Error(http.StatusForbidden, "feature toggles are read-only", fmt.Errorf("feature toggles are configured to be read-only"))
}
if featureMgmtCfg.UpdateWebhook == "" {
return response.Error(http.StatusInternalServerError, "feature toggles service is misconfigured", fmt.Errorf("[feature_management]update_webhook is not set"))
}
cmd := featuremgmt.UpdateFeatureTogglesCommand{}
if err := web.Bind(ctx.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
payload := UpdatePayload{
FeatureToggles: make(map[string]string, len(cmd.FeatureToggles)),
User: ctx.SignedInUser.Email,
}
for _, t := range cmd.FeatureToggles {
// make sure flag exists, and only continue if flag is writeable
if hs.featureManager.IsEditableFromAdminPage(t.Name) {
hs.log.Info("UpdateFeatureToggle: updating toggle", "toggle_name", t.Name, "enabled", t.Enabled, "username", ctx.SignedInUser.Login)
payload.FeatureToggles[t.Name] = strconv.FormatBool(t.Enabled)
} else {
hs.log.Warn("UpdateFeatureToggle: invalid toggle passed in", "toggle_name", t.Name)
return response.Error(http.StatusBadRequest, "invalid toggle passed in", fmt.Errorf("invalid toggle passed in: %s", t.Name))
}
}
err := sendWebhookUpdate(featureMgmtCfg, payload, hs.log)
if err != nil {
hs.log.Error("UpdateFeatureToggle: Failed to perform webhook request", "error", err)
return response.Respond(http.StatusBadRequest, "Failed to perform webhook request")
}
hs.featureManager.SetRestartRequired()
return response.Respond(http.StatusOK, "feature toggles updated successfully")
}
func (hs *HTTPServer) GetFeatureMgmtState(ctx *contextmodel.ReqContext) response.Response {
fmState := hs.featureManager.GetState()
return response.Respond(http.StatusOK, fmState)
}
type UpdatePayload struct {
FeatureToggles map[string]string `json:"feature_toggles"`
User string `json:"user"`
}
func sendWebhookUpdate(cfg setting.FeatureMgmtSettings, payload UpdatePayload, logger log.Logger) error {
data, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, cfg.UpdateWebhook, bytes.NewBuffer(data))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+cfg.UpdateWebhookToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer func() {
if err := resp.Body.Close(); err != nil {
logger.Warn("Failed to close response body", "err", err)
}
}()
if resp.StatusCode >= http.StatusBadRequest {
if body, err := io.ReadAll(resp.Body); err != nil {
return fmt.Errorf("SendWebhookUpdate failed with status=%d, error: %s", resp.StatusCode, string(body))
} else {
return fmt.Errorf("SendWebhookUpdate failed with status=%d, error: %w", resp.StatusCode, err)
}
}
return nil
}
-491
View File
@@ -1,491 +0,0 @@
package api
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"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/web/webtest"
)
func TestGetFeatureToggles(t *testing.T) {
readPermissions := []accesscontrol.Permission{{Action: accesscontrol.ActionFeatureManagementRead}}
t.Run("should not be able to get feature toggles without permissions", func(t *testing.T) {
result := runGetScenario(t, []*featuremgmt.FeatureFlag{}, []string{}, setting.FeatureMgmtSettings{}, []accesscontrol.Permission{}, http.StatusForbidden)
assert.Len(t, result, 0)
})
t.Run("should be able to get feature toggles with correct permissions", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: "toggle1",
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle2",
Stage: featuremgmt.FeatureStageGeneralAvailability,
},
}
disabled := []string{"toggle2"}
result := runGetScenario(t, features, disabled, setting.FeatureMgmtSettings{}, readPermissions, http.StatusOK)
assert.Len(t, result, 2)
t1, _ := findResult(t, result, "toggle1")
assert.True(t, t1.Enabled)
t2, _ := findResult(t, result, "toggle2")
assert.False(t, t2.Enabled)
})
t.Run("toggles hidden by config are not present in the response", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: "toggle1",
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle2",
Stage: featuremgmt.FeatureStageGeneralAvailability,
},
}
settings := setting.FeatureMgmtSettings{
HiddenToggles: map[string]struct{}{"toggle1": {}},
}
result := runGetScenario(t, features, []string{}, settings, readPermissions, http.StatusOK)
assert.Len(t, result, 1)
assert.Equal(t, "toggle2", result[0].Name)
})
t.Run("toggles that are read-only by config have the readOnly field set", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: "toggle1",
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle2",
Stage: featuremgmt.FeatureStageGeneralAvailability,
},
}
disabled := []string{"toggle2"}
settings := setting.FeatureMgmtSettings{
HiddenToggles: map[string]struct{}{"toggle1": {}},
ReadOnlyToggles: map[string]struct{}{"toggle2": {}},
AllowEditing: true,
UpdateWebhook: "bogus",
}
result := runGetScenario(t, features, disabled, settings, readPermissions, http.StatusOK)
assert.Len(t, result, 1)
assert.Equal(t, "toggle2", result[0].Name)
assert.True(t, result[0].ReadOnly)
})
t.Run("feature toggle defailts", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: "toggle1",
Stage: featuremgmt.FeatureStageUnknown,
}, {
Name: "toggle2",
Stage: featuremgmt.FeatureStageExperimental,
}, {
Name: "toggle3",
Stage: featuremgmt.FeatureStagePrivatePreview,
}, {
Name: "toggle4",
Stage: featuremgmt.FeatureStagePublicPreview,
AllowSelfServe: true,
}, {
Name: "toggle5",
Stage: featuremgmt.FeatureStageGeneralAvailability,
AllowSelfServe: true,
}, {
Name: "toggle6",
Stage: featuremgmt.FeatureStageDeprecated,
AllowSelfServe: true,
}, {
Name: "toggle7",
Stage: featuremgmt.FeatureStageGeneralAvailability,
AllowSelfServe: false,
},
}
t.Run("unknown, experimental, and private preview toggles are hidden by default", func(t *testing.T) {
result := runGetScenario(t, features, []string{}, setting.FeatureMgmtSettings{}, readPermissions, http.StatusOK)
assert.Len(t, result, 4)
_, ok := findResult(t, result, "toggle1")
assert.False(t, ok)
_, ok = findResult(t, result, "toggle2")
assert.False(t, ok)
_, ok = findResult(t, result, "toggle3")
assert.False(t, ok)
})
t.Run("only public preview and GA with AllowSelfServe are writeable", func(t *testing.T) {
settings := setting.FeatureMgmtSettings{
AllowEditing: true,
UpdateWebhook: "bogus",
}
result := runGetScenario(t, features, []string{}, settings, readPermissions, http.StatusOK)
assert.Len(t, result, 4)
t4, ok := findResult(t, result, "toggle4")
assert.True(t, ok)
assert.True(t, t4.ReadOnly)
t5, ok := findResult(t, result, "toggle5")
assert.True(t, ok)
assert.False(t, t5.ReadOnly)
t6, ok := findResult(t, result, "toggle6")
assert.True(t, ok)
assert.False(t, t6.ReadOnly)
})
t.Run("all toggles are read-only when server is misconfigured", func(t *testing.T) {
settings := setting.FeatureMgmtSettings{
AllowEditing: false,
UpdateWebhook: "",
}
result := runGetScenario(t, features, []string{}, settings, readPermissions, http.StatusOK)
assert.Len(t, result, 4)
t4, ok := findResult(t, result, "toggle4")
assert.True(t, ok)
assert.True(t, t4.ReadOnly)
t5, ok := findResult(t, result, "toggle5")
assert.True(t, ok)
assert.True(t, t5.ReadOnly)
t6, ok := findResult(t, result, "toggle6")
assert.True(t, ok)
assert.True(t, t6.ReadOnly)
})
})
}
func TestSetFeatureToggles(t *testing.T) {
writePermissions := []accesscontrol.Permission{{Action: accesscontrol.ActionFeatureManagementWrite}}
t.Run("fails without adequate permissions", func(t *testing.T) {
res := runSetScenario(t, nil, []string{}, nil, setting.FeatureMgmtSettings{}, []accesscontrol.Permission{}, http.StatusForbidden)
defer func() { require.NoError(t, res.Body.Close()) }()
})
t.Run("fails when toggle editing is not enabled", func(t *testing.T) {
res := runSetScenario(t, nil, []string{}, nil, setting.FeatureMgmtSettings{}, writePermissions, http.StatusForbidden)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "feature toggles are read-only", p["message"])
})
t.Run("fails when update toggle url is not set", func(t *testing.T) {
s := setting.FeatureMgmtSettings{
AllowEditing: true,
}
res := runSetScenario(t, nil, []string{}, nil, s, writePermissions, http.StatusInternalServerError)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "feature toggles service is misconfigured", p["message"])
})
t.Run("fails with non-existent toggle", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: "toggle1",
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle2",
Stage: featuremgmt.FeatureStageGeneralAvailability,
},
}
disabled := []string{"toggle2"}
updates := []featuremgmt.FeatureToggleDTO{
{
Name: "toggle3",
Enabled: true,
},
}
s := setting.FeatureMgmtSettings{
AllowEditing: true,
UpdateWebhook: "random",
}
res := runSetScenario(t, features, disabled, updates, s, writePermissions, http.StatusBadRequest)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "invalid toggle passed in", p["message"])
})
t.Run("fails with read-only toggles", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: featuremgmt.FlagFeatureToggleAdminPage,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle2",
Stage: featuremgmt.FeatureStagePublicPreview,
}, {
Name: "toggle3",
Stage: featuremgmt.FeatureStageGeneralAvailability,
},
}
disabled := []string{"toggle2", "toggle3"}
s := setting.FeatureMgmtSettings{
AllowEditing: true,
UpdateWebhook: "random",
ReadOnlyToggles: map[string]struct{}{
"toggle3": {},
},
}
t.Run("because it is the feature toggle admin page toggle", func(t *testing.T) {
updates := []featuremgmt.FeatureToggleDTO{
{
Name: featuremgmt.FlagFeatureToggleAdminPage,
Enabled: true,
},
}
res := runSetScenario(t, features, disabled, updates, s, writePermissions, http.StatusBadRequest)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "invalid toggle passed in", p["message"])
})
t.Run("because it is not GA or Deprecated", func(t *testing.T) {
updates := []featuremgmt.FeatureToggleDTO{
{
Name: "toggle2",
Enabled: true,
},
}
res := runSetScenario(t, features, disabled, updates, s, writePermissions, http.StatusBadRequest)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "invalid toggle passed in", p["message"])
})
t.Run("because it is configured to be read-only", func(t *testing.T) {
updates := []featuremgmt.FeatureToggleDTO{
{
Name: "toggle3",
Enabled: true,
},
}
res := runSetScenario(t, features, disabled, updates, s, writePermissions, http.StatusBadRequest)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "invalid toggle passed in", p["message"])
})
})
t.Run("when all conditions met", func(t *testing.T) {
features := []*featuremgmt.FeatureFlag{
{
Name: featuremgmt.FlagFeatureToggleAdminPage,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle2",
Stage: featuremgmt.FeatureStagePublicPreview,
}, {
Name: "toggle3",
Stage: featuremgmt.FeatureStageGeneralAvailability,
}, {
Name: "toggle4",
Stage: featuremgmt.FeatureStageGeneralAvailability,
AllowSelfServe: true,
}, {
Name: "toggle5",
Stage: featuremgmt.FeatureStageDeprecated,
AllowSelfServe: true,
},
}
disabled := []string{"toggle2", "toggle3", "toggle4", "toggle5"}
s := setting.FeatureMgmtSettings{
AllowEditing: true,
UpdateWebhook: "random",
UpdateWebhookToken: "token",
ReadOnlyToggles: map[string]struct{}{
"toggle3": {},
},
}
updates := []featuremgmt.FeatureToggleDTO{
{
Name: "toggle4",
Enabled: true,
}, {
Name: "toggle5",
Enabled: false,
},
}
t.Run("fail when webhook request is not successful", func(t *testing.T) {
webhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
}))
defer webhookServer.Close()
s.UpdateWebhook = webhookServer.URL
res := runSetScenario(t, features, disabled, updates, s, writePermissions, http.StatusBadRequest)
defer func() { require.NoError(t, res.Body.Close()) }()
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
})
t.Run("succeed when webhook request is successul", func(t *testing.T) {
webhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer "+s.UpdateWebhookToken, r.Header.Get("Authorization"))
var req UpdatePayload
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
assert.Equal(t, "true", req.FeatureToggles["toggle4"])
assert.Equal(t, "false", req.FeatureToggles["toggle5"])
w.WriteHeader(http.StatusOK)
}))
defer webhookServer.Close()
s.UpdateWebhook = webhookServer.URL
res := runSetScenario(t, features, disabled, updates, s, writePermissions, http.StatusOK)
defer func() { require.NoError(t, res.Body.Close()) }()
assert.Equal(t, http.StatusOK, res.StatusCode)
})
})
}
func findResult(t *testing.T, result []featuremgmt.FeatureToggleDTO, name string) (featuremgmt.FeatureToggleDTO, bool) {
t.Helper()
for _, t := range result {
if t.Name == name {
return t, true
}
}
return featuremgmt.FeatureToggleDTO{}, false
}
func readBody(t *testing.T, rc io.ReadCloser) map[string]any {
t.Helper()
b, err := io.ReadAll(rc)
require.NoError(t, err)
payload := map[string]any{}
require.NoError(t, json.Unmarshal(b, &payload))
return payload
}
func runGetScenario(
t *testing.T,
features []*featuremgmt.FeatureFlag,
disabled []string, // the flags that are disabled
settings setting.FeatureMgmtSettings,
permissions []accesscontrol.Permission,
expectedCode int,
) []featuremgmt.FeatureToggleDTO {
// Set up server and send request
cfg := setting.NewCfg()
fm := featuremgmt.WithFeatureManager(settings, append([]*featuremgmt.FeatureFlag{{
Name: featuremgmt.FlagFeatureToggleAdminPage,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}}, features...), disabled...)
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg
hs.Features = fm
hs.featureManager = fm
hs.orgService = orgtest.NewOrgServiceFake()
hs.userService = &usertest.FakeUserService{
ExpectedUser: &user.User{ID: 1},
}
hs.log = log.New("test")
})
req := webtest.RequestWithSignedInUser(server.NewGetRequest("/api/featuremgmt"), userWithPermissions(1, permissions))
res, err := server.SendJSON(req)
defer func() { require.NoError(t, res.Body.Close()) }()
// Do some general checks for every request
require.NoError(t, err)
require.Equal(t, expectedCode, res.StatusCode)
if res.StatusCode >= 400 {
return nil
}
var result []featuremgmt.FeatureToggleDTO
err = json.NewDecoder(res.Body).Decode(&result)
require.NoError(t, err)
for i := 0; i < len(result); {
ft := result[i]
// Always make sure admin page toggle is read-only, then remove it to make assertions easier
if ft.Name == featuremgmt.FlagFeatureToggleAdminPage {
assert.True(t, ft.ReadOnly)
result = append(result[:i], result[i+1:]...)
continue
}
// Make sure toggles explicitly marked "hidden" by config are hidden
if _, ok := cfg.FeatureManagement.HiddenToggles[ft.Name]; ok {
t.Fail()
}
// Make sure toggles explicitly marked "read only" by config are read only
if _, ok := cfg.FeatureManagement.ReadOnlyToggles[ft.Name]; ok {
assert.True(t, ft.ReadOnly)
}
i++
}
return result
}
func runSetScenario(
t *testing.T,
serverFeatures []*featuremgmt.FeatureFlag,
disabled []string, // the flags that are disabled
updateFeatures []featuremgmt.FeatureToggleDTO,
settings setting.FeatureMgmtSettings,
permissions []accesscontrol.Permission,
expectedCode int,
) *http.Response {
// Set up server and send request
cfg := setting.NewCfg()
features := featuremgmt.WithFeatureManager(settings, append([]*featuremgmt.FeatureFlag{{
Name: featuremgmt.FlagFeatureToggleAdminPage,
Stage: featuremgmt.FeatureStageGeneralAvailability,
}}, serverFeatures...), disabled...)
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg
hs.Features = features
hs.featureManager = features
hs.orgService = orgtest.NewOrgServiceFake()
hs.userService = &usertest.FakeUserService{
ExpectedUser: &user.User{ID: 1},
}
hs.log = log.New("test")
})
cmd := featuremgmt.UpdateFeatureTogglesCommand{
FeatureToggles: updateFeatures,
}
b, err := json.Marshal(cmd)
require.NoError(t, err)
req := webtest.RequestWithSignedInUser(server.NewPostRequest("/api/featuremgmt", bytes.NewReader(b)), userWithPermissions(1, permissions))
res, err := server.SendJSON(req)
require.NoError(t, err)
require.NotNil(t, res)
require.Equal(t, expectedCode, res.StatusCode)
return res
}
+1 -1
View File
@@ -19,8 +19,8 @@ import (
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
+3 -5
View File
@@ -25,8 +25,8 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/grafana/grafana/pkg/services/anonymous"
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/api/avatar"
"github.com/grafana/grafana/pkg/api/routing"
@@ -127,7 +127,6 @@ type HTTPServer struct {
RenderService rendering.Service
Cfg *setting.Cfg
Features featuremgmt.FeatureToggles
featureManager *featuremgmt.FeatureManager
SettingsProvider setting.Provider
HooksService *hooks.HooksService
navTreeService navtree.Service
@@ -235,7 +234,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
correlationsService correlations.Service, remoteCache *remotecache.RemoteCache, provisioningService provisioning.ProvisioningService,
accessControl accesscontrol.AccessControl, dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
contextHandler *contexthandler.ContextHandler, loggerMiddleware loggermw.Logger, features *featuremgmt.FeatureManager,
contextHandler *contexthandler.ContextHandler, loggerMiddleware loggermw.Logger, features featuremgmt.FeatureToggles,
alertNG *ngalert.AlertNG, libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
quotaService quota.Service, socialService social.Service, tracer tracing.Tracer,
encryptionService encryption.Internal, grafanaUpdateChecker *updatechecker.GrafanaService,
@@ -292,7 +291,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
QueryHistoryService: queryHistoryService,
CorrelationsService: correlationsService,
Features: features, // a read only view of the managers state
featureManager: features,
StorageService: storageService,
RemoteCacheService: remoteCache,
ProvisioningService: provisioningService,
+1
View File
@@ -92,6 +92,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
data := dtos.IndexViewData{
User: &dtos.CurrentUser{
Id: userID,
UID: c.UserUID, // << not set yet
IsSignedIn: c.IsSignedIn,
Login: c.Login,
Email: c.SignedInUser.GetEmail(),
+24
View File
@@ -10,10 +10,14 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/util/errutil/errhttp"
"github.com/grafana/grafana/pkg/web"
)
@@ -33,6 +37,26 @@ func (hs *HTTPServer) handleQueryMetricsError(err error) *response.NormalRespons
return response.ErrOrFallback(http.StatusInternalServerError, "Query data error", err)
}
// metrics.go
func (hs *HTTPServer) getDSQueryEndpoint() web.Handler {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesQueryServiceRewrite) {
// DEV ONLY FEATURE FLAG!
// rewrite requests from /ds/query to the new query service
namespaceMapper := request.GetNamespaceMapper(hs.Cfg)
return func(w http.ResponseWriter, r *http.Request) {
user, err := appcontext.User(r.Context())
if err != nil || user == nil {
errhttp.Write(r.Context(), fmt.Errorf("no user"), w)
return
}
r.URL.Path = "/apis/query.grafana.app/v0alpha1/namespaces/" + namespaceMapper(user.OrgID) + "/query"
hs.clientConfigProvider.DirectlyServeHTTP(w, r)
}
}
return routing.Wrap(hs.QueryMetricsV2)
}
// QueryMetricsV2 returns query metrics.
// swagger:route POST /ds/query ds queryMetricsWithExpressions
//
+2 -2
View File
@@ -15,10 +15,10 @@ import (
"github.com/grafana/grafana/pkg/apis/playlist/v0alpha1"
"github.com/grafana/grafana/pkg/middleware"
internalplaylist "github.com/grafana/grafana/pkg/registry/apis/playlist"
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/playlist"
"github.com/grafana/grafana/pkg/util/errutil/errhttp"
"github.com/grafana/grafana/pkg/web"
+2
View File
@@ -82,6 +82,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
}
usr, err := userSvc.Create(context.Background(), &createUserCmd)
require.NoError(t, err)
theUserUID := usr.UID
sc.handlerFunc = hs.GetUserByID
@@ -108,6 +109,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
expected := user.UserProfileDTO{
ID: 1,
UID: theUserUID, // from original request
Email: "user@test.com",
Name: "user",
Login: "loginuser",
@@ -0,0 +1,6 @@
// +k8s:deepcopy-gen=package
// +k8s:openapi-gen=true
// +k8s:defaulter-gen=TypeMeta
// +groupName=dashboardsnapshot.grafana.app
package v0alpha1
@@ -0,0 +1,25 @@
package v0alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
)
const (
GROUP = "dashboardsnapshot.grafana.app"
VERSION = "v0alpha1"
APIVERSION = GROUP + "/" + VERSION
)
var DashboardSnapshotResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"dashboardsnapshots", "dashboardsnapshot", "DashboardSnapshot",
func() runtime.Object { return &DashboardSnapshot{} },
func() runtime.Object { return &DashboardSnapshotList{} },
)
var (
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION}
)
@@ -0,0 +1,136 @@
package v0alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DashboardSnapshot struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`
// Snapshot summary info
Spec SnapshotInfo `json:"spec"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DashboardSnapshotList struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ListMeta `json:"metadata,omitempty"`
Items []DashboardSnapshot `json:"items,omitempty"`
}
type SnapshotInfo struct {
Title string `json:"title,omitempty"`
// Optionally auto-remove the snapshot at a future date
Expires int64 `json:"expires,omitempty"`
// When set to true, the snapshot exists in a remote server
External bool `json:"external,omitempty"`
// The external URL where the snapshot can be seen
ExternalURL string `json:"externalUrl,omitempty"`
// The URL that created the dashboard originally
OriginalUrl string `json:"originalUrl,omitempty"`
// Snapshot creation timestamp
Timestamp string `json:"timestamp,omitempty"`
}
// This is returned from the POST command
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DashboardSnapshotWithDeleteKey struct {
DashboardSnapshot `json:",inline"`
// The delete key is only returned when the item is created. It is not returned from a get request
DeleteKey string `json:"deleteKey,omitempty"`
}
// This is the snapshot returned from the subresource
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type FullDashboardSnapshot struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`
// Snapshot summary info
Info SnapshotInfo `json:"info"`
// The raw dashboard (unstructured for now)
Dashboard common.Unstructured `json:"dashboard"`
}
// Each tenant, may have different sharing options
// This is currently set using custom.ini, but multi-tenant support will need
// to be managed differently
type SnapshotSharingOptions struct {
SnapshotsEnabled bool `json:"snapshotEnabled"`
ExternalSnapshotURL string `json:"externalSnapshotURL,omitempty"`
ExternalSnapshotName string `json:"externalSnapshotName,omitempty"`
ExternalEnabled bool `json:"externalEnabled,omitempty"`
}
// These are the values expected to be sent from an end user
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DashboardCreateCommand struct {
metav1.TypeMeta `json:",inline"`
// Snapshot name
// required:false
Name string `json:"name"`
// The complete dashboard model.
// required:true
Dashboard *common.Unstructured `json:"dashboard" binding:"Required"`
// When the snapshot should expire in seconds in seconds. Default is never to expire.
// required:false
// default:0
Expires int64 `json:"expires"`
// these are passed when storing an external snapshot ref
// Save the snapshot on an external server rather than locally.
// required:false
// default: false
External bool `json:"external"`
}
// The create response
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DashboardCreateResponse struct {
metav1.TypeMeta `json:",inline"`
// The unique key
Key string `json:"key"`
// A unique key that will allow delete
DeleteKey string `json:"deleteKey"`
// Absolute URL to show the dashboard
URL string `json:"url"`
// URL that will delete the response
DeleteURL string `json:"deleteUrl"`
}
// Represents an options object that must be named for each namespace/team/user
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type SharingOptions struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// Show the options inline
Spec SnapshotSharingOptions `json:"spec"`
}
// Represents a list of namespaced options
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type SharingOptionsList struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ListMeta `json:"metadata,omitempty"`
Items []SharingOptions `json:"items,omitempty"`
}
@@ -0,0 +1,271 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by deepcopy-gen. DO NOT EDIT.
package v0alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardCreateCommand) DeepCopyInto(out *DashboardCreateCommand) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.Dashboard != nil {
in, out := &in.Dashboard, &out.Dashboard
*out = (*in).DeepCopy()
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardCreateCommand.
func (in *DashboardCreateCommand) DeepCopy() *DashboardCreateCommand {
if in == nil {
return nil
}
out := new(DashboardCreateCommand)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DashboardCreateCommand) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardCreateResponse) DeepCopyInto(out *DashboardCreateResponse) {
*out = *in
out.TypeMeta = in.TypeMeta
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardCreateResponse.
func (in *DashboardCreateResponse) DeepCopy() *DashboardCreateResponse {
if in == nil {
return nil
}
out := new(DashboardCreateResponse)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DashboardCreateResponse) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardSnapshot) DeepCopyInto(out *DashboardSnapshot) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSnapshot.
func (in *DashboardSnapshot) DeepCopy() *DashboardSnapshot {
if in == nil {
return nil
}
out := new(DashboardSnapshot)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DashboardSnapshot) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardSnapshotList) DeepCopyInto(out *DashboardSnapshotList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]DashboardSnapshot, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSnapshotList.
func (in *DashboardSnapshotList) DeepCopy() *DashboardSnapshotList {
if in == nil {
return nil
}
out := new(DashboardSnapshotList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DashboardSnapshotList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DashboardSnapshotWithDeleteKey) DeepCopyInto(out *DashboardSnapshotWithDeleteKey) {
*out = *in
in.DashboardSnapshot.DeepCopyInto(&out.DashboardSnapshot)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSnapshotWithDeleteKey.
func (in *DashboardSnapshotWithDeleteKey) DeepCopy() *DashboardSnapshotWithDeleteKey {
if in == nil {
return nil
}
out := new(DashboardSnapshotWithDeleteKey)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DashboardSnapshotWithDeleteKey) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FullDashboardSnapshot) DeepCopyInto(out *FullDashboardSnapshot) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Info = in.Info
in.Dashboard.DeepCopyInto(&out.Dashboard)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FullDashboardSnapshot.
func (in *FullDashboardSnapshot) DeepCopy() *FullDashboardSnapshot {
if in == nil {
return nil
}
out := new(FullDashboardSnapshot)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *FullDashboardSnapshot) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SharingOptions) DeepCopyInto(out *SharingOptions) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SharingOptions.
func (in *SharingOptions) DeepCopy() *SharingOptions {
if in == nil {
return nil
}
out := new(SharingOptions)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *SharingOptions) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SharingOptionsList) DeepCopyInto(out *SharingOptionsList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]SharingOptions, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SharingOptionsList.
func (in *SharingOptionsList) DeepCopy() *SharingOptionsList {
if in == nil {
return nil
}
out := new(SharingOptionsList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *SharingOptionsList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SnapshotInfo) DeepCopyInto(out *SnapshotInfo) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotInfo.
func (in *SnapshotInfo) DeepCopy() *SnapshotInfo {
if in == nil {
return nil
}
out := new(SnapshotInfo)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SnapshotSharingOptions) DeepCopyInto(out *SnapshotSharingOptions) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotSharingOptions.
func (in *SnapshotSharingOptions) DeepCopy() *SnapshotSharingOptions {
if in == nil {
return nil
}
out := new(SnapshotSharingOptions)
in.DeepCopyInto(out)
return out
}
@@ -0,0 +1,19 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by defaulter-gen. DO NOT EDIT.
package v0alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// RegisterDefaults adds defaulters functions to the given scheme.
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
return nil
}
@@ -0,0 +1,521 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by openapi-gen. DO NOT EDIT.
// This file was autogenerated by openapi-gen. Do not edit it manually!
package v0alpha1
import (
common "k8s.io/kube-openapi/pkg/common"
spec "k8s.io/kube-openapi/pkg/validation/spec"
)
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardCreateCommand": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateCommand(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardCreateResponse": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateResponse(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshot": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshot(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshotList": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshotList(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshotWithDeleteKey": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshotWithDeleteKey(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.FullDashboardSnapshot": schema_pkg_apis_dashboardsnapshot_v0alpha1_FullDashboardSnapshot(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SharingOptions": schema_pkg_apis_dashboardsnapshot_v0alpha1_SharingOptions(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SharingOptionsList": schema_pkg_apis_dashboardsnapshot_v0alpha1_SharingOptionsList(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo": schema_pkg_apis_dashboardsnapshot_v0alpha1_SnapshotInfo(ref),
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotSharingOptions": schema_pkg_apis_dashboardsnapshot_v0alpha1_SnapshotSharingOptions(ref),
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateCommand(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "These are the values expected to be sent from an end user",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"name": {
SchemaProps: spec.SchemaProps{
Description: "Snapshot name required:false",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"dashboard": {
SchemaProps: spec.SchemaProps{
Description: "The complete dashboard model. required:true",
Ref: ref("github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"),
},
},
"expires": {
SchemaProps: spec.SchemaProps{
Description: "When the snapshot should expire in seconds in seconds. Default is never to expire. required:false default:0",
Default: 0,
Type: []string{"integer"},
Format: "int64",
},
},
"external": {
SchemaProps: spec.SchemaProps{
Description: "these are passed when storing an external snapshot ref Save the snapshot on an external server rather than locally. required:false default: false",
Default: false,
Type: []string{"boolean"},
Format: "",
},
},
},
Required: []string{"name", "dashboard", "expires", "external"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateResponse(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "The create response",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"key": {
SchemaProps: spec.SchemaProps{
Description: "The unique key",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"deleteKey": {
SchemaProps: spec.SchemaProps{
Description: "A unique key that will allow delete",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"url": {
SchemaProps: spec.SchemaProps{
Description: "Absolute URL to show the dashboard",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"deleteUrl": {
SchemaProps: spec.SchemaProps{
Description: "URL that will delete the response",
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"key", "deleteKey", "url", "deleteUrl"},
},
},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshot(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Description: "Snapshot summary info",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo"),
},
},
},
Required: []string{"spec"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshotList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshot"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshot", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshotWithDeleteKey(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "This is returned from the POST command",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Description: "Snapshot summary info",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo"),
},
},
"deleteKey": {
SchemaProps: spec.SchemaProps{
Description: "The delete key is only returned when the item is created. It is not returned from a get request",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"spec"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_FullDashboardSnapshot(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "This is the snapshot returned from the subresource",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"info": {
SchemaProps: spec.SchemaProps{
Description: "Snapshot summary info",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo"),
},
},
"dashboard": {
SchemaProps: spec.SchemaProps{
Description: "The raw dashboard (unstructured for now)",
Ref: ref("github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured"),
},
},
},
Required: []string{"info", "dashboard"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured", "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_SharingOptions(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Represents an options object that must be named for each namespace/team/user",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Description: "Show the options inline",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotSharingOptions"),
},
},
},
Required: []string{"spec"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotSharingOptions", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_SharingOptionsList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Represents a list of namespaced options",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SharingOptions"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SharingOptions", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_SnapshotInfo(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"title": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"expires": {
SchemaProps: spec.SchemaProps{
Description: "Optionally auto-remove the snapshot at a future date",
Type: []string{"integer"},
Format: "int64",
},
},
"external": {
SchemaProps: spec.SchemaProps{
Description: "When set to true, the snapshot exists in a remote server",
Type: []string{"boolean"},
Format: "",
},
},
"externalUrl": {
SchemaProps: spec.SchemaProps{
Description: "The external URL where the snapshot can be seen",
Type: []string{"string"},
Format: "",
},
},
"originalUrl": {
SchemaProps: spec.SchemaProps{
Description: "The URL that created the dashboard originally",
Type: []string{"string"},
Format: "",
},
},
"timestamp": {
SchemaProps: spec.SchemaProps{
Description: "Snapshot creation timestamp",
Type: []string{"string"},
Format: "",
},
},
},
},
},
}
}
func schema_pkg_apis_dashboardsnapshot_v0alpha1_SnapshotSharingOptions(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Each tenant, may have different sharing options This is currently set using custom.ini, but multi-tenant support will need to be managed differently",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"snapshotEnabled": {
SchemaProps: spec.SchemaProps{
Default: false,
Type: []string{"boolean"},
Format: "",
},
},
"externalSnapshotURL": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"externalSnapshotName": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"externalEnabled": {
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
Format: "",
},
},
},
Required: []string{"snapshotEnabled"},
},
},
}
}
@@ -0,0 +1,3 @@
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1,DashboardCreateResponse,DeleteURL
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1,SnapshotInfo,ExternalURL
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1,SnapshotSharingOptions,SnapshotsEnabled
+6 -3
View File
@@ -78,8 +78,11 @@ type FeatureTogglesList struct {
type ResolvedToggleState struct {
metav1.TypeMeta `json:",inline"`
// Can any flag be updated
Writeable bool `json:"writeable,omitempty"`
// The user is allowed to edit feature toggles on this system
AllowEditing bool `json:"allowEditing,omitempty"`
// The system has changes that require still require a restart
RestartRequired bool `json:"restartRequired,omitempty"`
// The currently enabled flags
Enabled map[string]bool `json:"enabled,omitempty"`
@@ -99,7 +102,7 @@ type ToggleStatus struct {
Enabled bool `json:"enabled"`
// Can this flag be updated
Writeable bool `json:"writeable,omitempty"`
Writeable bool `json:"writeable"`
// Where was the value configured
// eg: startup | tenant|org | user | browser
@@ -318,9 +318,16 @@ func schema_pkg_apis_featuretoggle_v0alpha1_ResolvedToggleState(ref common.Refer
Format: "",
},
},
"writeable": {
"allowEditing": {
SchemaProps: spec.SchemaProps{
Description: "Can any flag be updated",
Description: "The user is allowed to edit feature toggles on this system",
Type: []string{"boolean"},
Format: "",
},
},
"restartRequired": {
SchemaProps: spec.SchemaProps{
Description: "The system has changes that require still require a restart",
Type: []string{"boolean"},
Format: "",
},
@@ -395,6 +402,7 @@ func schema_pkg_apis_featuretoggle_v0alpha1_ToggleStatus(ref common.ReferenceCal
"writeable": {
SchemaProps: spec.SchemaProps{
Description: "Can this flag be updated",
Default: false,
Type: []string{"boolean"},
Format: "",
},
@@ -413,7 +421,7 @@ func schema_pkg_apis_featuretoggle_v0alpha1_ToggleStatus(ref common.ReferenceCal
},
},
},
Required: []string{"name", "enabled"},
Required: []string{"name", "enabled", "writeable"},
},
},
Dependencies: []string{
+6
View File
@@ -0,0 +1,6 @@
// +k8s:deepcopy-gen=package
// +k8s:openapi-gen=true
// +k8s:defaulter-gen=TypeMeta
// +groupName=query.grafana.app
package v0alpha1
+211
View File
@@ -0,0 +1,211 @@
package v0alpha1
import (
"encoding/json"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
// Generic query request with shared time across all values
// Copied from: https://github.com/grafana/grafana/blob/main/pkg/api/dtos/models.go#L62
type GenericQueryRequest struct {
metav1.TypeMeta `json:",inline"`
// From Start time in epoch timestamps in milliseconds or relative using Grafana time units.
// example: now-1h
From string `json:"from,omitempty"`
// To End time in epoch timestamps in milliseconds or relative using Grafana time units.
// example: now
To string `json:"to,omitempty"`
// queries.refId – Specifies an identifier of the query. Is optional and default to “A”.
// queries.datasourceId – Specifies the data source to be queried. Each query in the request must have an unique datasourceId.
// queries.maxDataPoints - Species maximum amount of data points that dashboard panel can render. Is optional and default to 100.
// queries.intervalMs - Specifies the time interval in milliseconds of time series. Is optional and defaults to 1000.
// required: true
// example: [ { "refId": "A", "intervalMs": 86400000, "maxDataPoints": 1092, "datasource":{ "uid":"PD8C576611E62080A" }, "rawSql": "SELECT 1 as valueOne, 2 as valueTwo", "format": "table" } ]
Queries []GenericDataQuery `json:"queries"`
// required: false
Debug bool `json:"debug,omitempty"`
}
type DataSourceRef struct {
// The datasource plugin type
Type string `json:"type"`
// Datasource UID
UID string `json:"uid"`
}
// GenericDataQuery is a replacement for `dtos.MetricRequest` that provides more explicit types
type GenericDataQuery struct {
// RefID is the unique identifier of the query, set by the frontend call.
RefID string `json:"refId"`
// TimeRange represents the query range
// NOTE: unlike generic /ds/query, we can now send explicit time values in each query
TimeRange *TimeRange `json:"timeRange,omitempty"`
// The datasource
Datasource *DataSourceRef `json:"datasource,omitempty"`
// Deprecated -- use datasource ref instead
DatasourceId int64 `json:"datasourceId,omitempty"`
// QueryType is an optional identifier for the type of query.
// It can be used to distinguish different types of queries.
QueryType string `json:"queryType,omitempty"`
// MaxDataPoints is the maximum number of data points that should be returned from a time series query.
MaxDataPoints int64 `json:"maxDataPoints,omitempty"`
// Interval is the suggested duration between time points in a time series query.
IntervalMS float64 `json:"intervalMs,omitempty"`
// true if query is disabled (ie should not be returned to the dashboard)
// Note this does not always imply that the query should not be executed since
// the results from a hidden query may be used as the input to other queries (SSE etc)
Hide bool `json:"hide,omitempty"`
// Additional Properties (that live at the root)
props map[string]any `json:"-"`
}
// TimeRange represents a time range for a query and is a property of DataQuery.
type TimeRange struct {
// From is the start time of the query.
From string `json:"from"`
// To is the end time of the query.
To string `json:"to"`
}
func (g *GenericDataQuery) AdditionalProperties() map[string]any {
if g.props == nil {
g.props = make(map[string]any)
}
return g.props
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (g *GenericDataQuery) DeepCopyInto(out *GenericDataQuery) {
*out = *g
if g.props != nil {
out.props = runtime.DeepCopyJSON(g.props)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericDataQuery.
func (g *GenericDataQuery) DeepCopy() *GenericDataQuery {
if g == nil {
return nil
}
out := new(GenericDataQuery)
g.DeepCopyInto(out)
return out
}
// MarshalJSON ensures that the unstructured object produces proper
// JSON when passed to Go's standard JSON library.
func (g GenericDataQuery) MarshalJSON() ([]byte, error) {
vals := map[string]any{}
if g.props != nil {
for k, v := range g.props {
vals[k] = v
}
}
vals["refId"] = g.RefID
if g.Datasource.Type != "" || g.Datasource.UID != "" {
vals["datasource"] = g.Datasource
}
if g.DatasourceId > 0 {
vals["datasourceId"] = g.DatasourceId
}
if g.IntervalMS > 0 {
vals["intervalMs"] = g.IntervalMS
}
if g.MaxDataPoints > 0 {
vals["maxDataPoints"] = g.MaxDataPoints
}
return json.Marshal(vals)
}
// UnmarshalJSON ensures that the unstructured object properly decodes
// JSON when passed to Go's standard JSON library.
func (g *GenericDataQuery) UnmarshalJSON(b []byte) error {
vals := map[string]any{}
err := json.Unmarshal(b, &vals)
if err != nil {
return err
}
key := "refId"
v, ok := vals[key]
if ok {
g.RefID, ok = v.(string)
if !ok {
return fmt.Errorf("expected string refid (got: %t)", v)
}
delete(vals, key)
}
key = "datasource"
v, ok = vals[key]
if ok {
wrap, ok := v.(map[string]any)
if ok {
g.Datasource = &DataSourceRef{}
g.Datasource.Type, _ = wrap["type"].(string)
g.Datasource.UID, _ = wrap["uid"].(string)
delete(vals, key)
} else {
// Old old queries may arrive with just the name
name, ok := v.(string)
if !ok {
return fmt.Errorf("expected datasource as object (got: %t)", v)
}
g.Datasource = &DataSourceRef{}
g.Datasource.UID = name // Not great, but the lookup function will try its best to resolve
delete(vals, key)
}
}
key = "intervalMs"
v, ok = vals[key]
if ok {
g.IntervalMS, ok = v.(float64)
if !ok {
return fmt.Errorf("expected intervalMs as float (got: %t)", v)
}
delete(vals, key)
}
key = "maxDataPoints"
v, ok = vals[key]
if ok {
count, ok := v.(float64)
if !ok {
return fmt.Errorf("expected maxDataPoints as number (got: %t)", v)
}
g.MaxDataPoints = int64(count)
delete(vals, key)
}
key = "datasourceId"
v, ok = vals[key]
if ok {
count, ok := v.(float64)
if !ok {
return fmt.Errorf("expected datasourceId as number (got: %t)", v)
}
g.DatasourceId = int64(count)
delete(vals, key)
}
g.props = vals
return nil
}
+75
View File
@@ -0,0 +1,75 @@
package v0alpha1_test
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apis/query/v0alpha1"
)
func TestParseQueriesIntoQueryDataRequest(t *testing.T) {
request := []byte(`{
"queries": [
{
"refId": "A",
"datasource": {
"type": "grafana-googlesheets-datasource",
"uid": "b1808c48-9fc9-4045-82d7-081781f8a553"
},
"cacheDurationSeconds": 300,
"spreadsheet": "spreadsheetID",
"datasourceId": 4,
"intervalMs": 30000,
"maxDataPoints": 794
},
{
"refId": "Z",
"datasource": "old",
"maxDataPoints": 10,
"timeRange": {
"from": "100",
"to": "200"
}
}
],
"from": "1692624667389",
"to": "1692646267389"
}`)
req := &v0alpha1.GenericQueryRequest{}
err := json.Unmarshal(request, req)
require.NoError(t, err)
require.Len(t, req.Queries, 2)
require.Equal(t, "b1808c48-9fc9-4045-82d7-081781f8a553", req.Queries[0].Datasource.UID)
require.Equal(t, "spreadsheetID", req.Queries[0].AdditionalProperties()["spreadsheet"])
// Write the query (with additional spreadsheetID) to JSON
out, err := json.MarshalIndent(req.Queries[0], "", " ")
require.NoError(t, err)
// And read it back with standard JSON marshal functions
query := &v0alpha1.GenericDataQuery{}
err = json.Unmarshal(out, query)
require.NoError(t, err)
require.Equal(t, "spreadsheetID", query.AdditionalProperties()["spreadsheet"])
// The second query has an explicit time range, and legacy datasource name
out, err = json.MarshalIndent(req.Queries[1], "", " ")
require.NoError(t, err)
// fmt.Printf("%s\n", string(out))
require.JSONEq(t, `{
"datasource": {
"type": "", ` /* NOTE! this implies legacy naming */ +`
"uid": "old"
},
"maxDataPoints": 10,
"refId": "Z",
"timeRange": {
"from": "100",
"to": "200"
}
}`, string(out))
}
+25
View File
@@ -0,0 +1,25 @@
package v0alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
)
const (
GROUP = "query.grafana.app"
VERSION = "v0alpha1"
APIVERSION = GROUP + "/" + VERSION
)
var DataSourceApiServerResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"datasourceapiservers", "datasourceapiserver", "DataSourceApiServer",
func() runtime.Object { return &DataSourceApiServer{} },
func() runtime.Object { return &DataSourceApiServerList{} },
)
var (
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION}
)
+64
View File
@@ -0,0 +1,64 @@
package v0alpha1
import (
"encoding/json"
"github.com/grafana/grafana-plugin-sdk-go/backend"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
openapi "k8s.io/kube-openapi/pkg/common"
spec "k8s.io/kube-openapi/pkg/validation/spec"
)
// Wraps backend.QueryDataResponse, however it includes TypeMeta and implements runtime.Object
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type QueryDataResponse struct {
metav1.TypeMeta `json:",inline"`
// Backend wrapper (external dependency)
backend.QueryDataResponse
}
// Expose backend DataResponse in OpenAPI (yes this still requires some serious love!)
func (r QueryDataResponse) OpenAPIDefinition() openapi.OpenAPIDefinition {
return openapi.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{Allows: true},
},
VendorExtensible: spec.VendorExtensible{
Extensions: map[string]interface{}{
"x-kubernetes-preserve-unknown-fields": true,
},
},
},
}
}
// MarshalJSON writes the results as json
func (r QueryDataResponse) MarshalJSON() ([]byte, error) {
return r.QueryDataResponse.MarshalJSON()
}
// UnmarshalJSON will read JSON into a QueryDataResponse
func (r *QueryDataResponse) UnmarshalJSON(b []byte) error {
return r.QueryDataResponse.UnmarshalJSON(b)
}
func (r *QueryDataResponse) DeepCopy() *QueryDataResponse {
if r == nil {
return nil
}
// /!\ The most dumb approach, but OK for now...
// likely best to move DeepCopy into SDK
out := &QueryDataResponse{}
body, _ := json.Marshal(r.QueryDataResponse)
_ = json.Unmarshal(body, &out.QueryDataResponse)
return out
}
func (r *QueryDataResponse) DeepCopyInto(out *QueryDataResponse) {
clone := r.DeepCopy()
*out = *clone
}
+65
View File
@@ -0,0 +1,65 @@
package v0alpha1
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/backend"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// The query runner interface
type QueryRunner interface {
// Runs the query as the user in context
ExecuteQueryData(ctx context.Context,
// The k8s group for the datasource (pluginId)
datasource schema.GroupVersion,
// The datasource name/uid
name string,
// The raw backend query objects
query []GenericDataQuery,
) (*backend.QueryDataResponse, error)
}
type DataSourceApiServerRegistry interface {
// Get the group and preferred version for a plugin
GetDatasourceGroupVersion(pluginId string) (schema.GroupVersion, error)
// Get the list of available datasource api servers
// The values will be managed though API discovery/reconciliation
GetDatasourceApiServers(ctx context.Context) (*DataSourceApiServerList, error)
}
// The data source resource is a reflection of the individual datasource instances
// that are exposed in the groups: {datasource}.datasource.grafana.app
// The status is updated periodically.
// The name is the plugin id
//
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DataSourceApiServer struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// The display name
Title string `json:"title"`
// Describe the plugin
Description string `json:"description,omitempty"`
// The group + preferred version
GroupVersion string `json:"groupVersion"`
// Possible alternative plugin IDs
AliasIDs []string `json:"aliasIDs,omitempty"`
}
// List of datasource plugins
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type DataSourceApiServerList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []DataSourceApiServer `json:"items,omitempty"`
}
@@ -0,0 +1,140 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by deepcopy-gen. DO NOT EDIT.
package v0alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DataSourceApiServer) DeepCopyInto(out *DataSourceApiServer) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.AliasIDs != nil {
in, out := &in.AliasIDs, &out.AliasIDs
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSourceApiServer.
func (in *DataSourceApiServer) DeepCopy() *DataSourceApiServer {
if in == nil {
return nil
}
out := new(DataSourceApiServer)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DataSourceApiServer) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DataSourceApiServerList) DeepCopyInto(out *DataSourceApiServerList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]DataSourceApiServer, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSourceApiServerList.
func (in *DataSourceApiServerList) DeepCopy() *DataSourceApiServerList {
if in == nil {
return nil
}
out := new(DataSourceApiServerList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DataSourceApiServerList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DataSourceRef) DeepCopyInto(out *DataSourceRef) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSourceRef.
func (in *DataSourceRef) DeepCopy() *DataSourceRef {
if in == nil {
return nil
}
out := new(DataSourceRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GenericQueryRequest) DeepCopyInto(out *GenericQueryRequest) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.Queries != nil {
in, out := &in.Queries, &out.Queries
*out = make([]GenericDataQuery, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericQueryRequest.
func (in *GenericQueryRequest) DeepCopy() *GenericQueryRequest {
if in == nil {
return nil
}
out := new(GenericQueryRequest)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *QueryDataResponse) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TimeRange) DeepCopyInto(out *TimeRange) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeRange.
func (in *TimeRange) DeepCopy() *TimeRange {
if in == nil {
return nil
}
out := new(TimeRange)
in.DeepCopyInto(out)
return out
}
@@ -0,0 +1,19 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by defaulter-gen. DO NOT EDIT.
package v0alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// RegisterDefaults adds defaulters functions to the given scheme.
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
return nil
}
@@ -0,0 +1,343 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by openapi-gen. DO NOT EDIT.
// This file was autogenerated by openapi-gen. Do not edit it manually!
package v0alpha1
import (
common "k8s.io/kube-openapi/pkg/common"
spec "k8s.io/kube-openapi/pkg/validation/spec"
)
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServer": schema_pkg_apis_query_v0alpha1_DataSourceApiServer(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServerList": schema_pkg_apis_query_v0alpha1_DataSourceApiServerList(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceRef": schema_pkg_apis_query_v0alpha1_DataSourceRef(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.GenericDataQuery": schema_pkg_apis_query_v0alpha1_GenericDataQuery(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.GenericQueryRequest": schema_pkg_apis_query_v0alpha1_GenericQueryRequest(ref),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.QueryDataResponse": QueryDataResponse{}.OpenAPIDefinition(),
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.TimeRange": schema_pkg_apis_query_v0alpha1_TimeRange(ref),
}
}
func schema_pkg_apis_query_v0alpha1_DataSourceApiServer(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "The data source resource is a reflection of the individual datasource instances that are exposed in the groups: {datasource}.datasource.grafana.app The status is updated periodically. The name is the plugin id",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"title": {
SchemaProps: spec.SchemaProps{
Description: "The display name",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"description": {
SchemaProps: spec.SchemaProps{
Description: "Describe the plugin",
Type: []string{"string"},
Format: "",
},
},
"groupVersion": {
SchemaProps: spec.SchemaProps{
Description: "The group + preferred version",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"aliasIDs": {
SchemaProps: spec.SchemaProps{
Description: "Possible alternative plugin IDs",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
Required: []string{"title", "groupVersion"},
},
},
Dependencies: []string{
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_query_v0alpha1_DataSourceApiServerList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "List of datasource plugins",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"metadata": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
},
},
"items": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServer"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceApiServer", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_query_v0alpha1_DataSourceRef(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"type": {
SchemaProps: spec.SchemaProps{
Description: "The datasource plugin type",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"uid": {
SchemaProps: spec.SchemaProps{
Description: "Datasource UID",
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"type", "uid"},
},
},
}
}
func schema_pkg_apis_query_v0alpha1_GenericDataQuery(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "GenericDataQuery is a replacement for `dtos.MetricRequest` that provides more explicit types",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"refId": {
SchemaProps: spec.SchemaProps{
Description: "RefID is the unique identifier of the query, set by the frontend call.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"timeRange": {
SchemaProps: spec.SchemaProps{
Description: "TimeRange represents the query range NOTE: unlike generic /ds/query, we can now send explicit time values in each query",
Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1.TimeRange"),
},
},
"datasource": {
SchemaProps: spec.SchemaProps{
Description: "The datasource",
Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceRef"),
},
},
"datasourceId": {
SchemaProps: spec.SchemaProps{
Description: "Deprecated -- use datasource ref instead",
Type: []string{"integer"},
Format: "int64",
},
},
"queryType": {
SchemaProps: spec.SchemaProps{
Description: "QueryType is an optional identifier for the type of query. It can be used to distinguish different types of queries.",
Type: []string{"string"},
Format: "",
},
},
"maxDataPoints": {
SchemaProps: spec.SchemaProps{
Description: "MaxDataPoints is the maximum number of data points that should be returned from a time series query.",
Type: []string{"integer"},
Format: "int64",
},
},
"intervalMs": {
SchemaProps: spec.SchemaProps{
Description: "Interval is the suggested duration between time points in a time series query.",
Type: []string{"number"},
Format: "double",
},
},
"hide": {
SchemaProps: spec.SchemaProps{
Description: "true if query is disabled (ie should not be returned to the dashboard) Note this does not always imply that the query should not be executed since the results from a hidden query may be used as the input to other queries (SSE etc)",
Type: []string{"boolean"},
Format: "",
},
},
},
Required: []string{"refId"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.DataSourceRef", "github.com/grafana/grafana/pkg/apis/query/v0alpha1.TimeRange"},
}
}
func schema_pkg_apis_query_v0alpha1_GenericQueryRequest(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Generic query request with shared time across all values Copied from: https://github.com/grafana/grafana/blob/main/pkg/api/dtos/models.go#L62",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": {
SchemaProps: spec.SchemaProps{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Type: []string{"string"},
Format: "",
},
},
"apiVersion": {
SchemaProps: spec.SchemaProps{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Type: []string{"string"},
Format: "",
},
},
"from": {
SchemaProps: spec.SchemaProps{
Description: "From Start time in epoch timestamps in milliseconds or relative using Grafana time units. example: now-1h",
Type: []string{"string"},
Format: "",
},
},
"to": {
SchemaProps: spec.SchemaProps{
Description: "To End time in epoch timestamps in milliseconds or relative using Grafana time units. example: now",
Type: []string{"string"},
Format: "",
},
},
"queries": {
SchemaProps: spec.SchemaProps{
Description: "queries.refId – Specifies an identifier of the query. Is optional and default to “A”. queries.datasourceId – Specifies the data source to be queried. Each query in the request must have an unique datasourceId. queries.maxDataPoints - Species maximum amount of data points that dashboard panel can render. Is optional and default to 100. queries.intervalMs - Specifies the time interval in milliseconds of time series. Is optional and defaults to 1000. required: true example: [ { \"refId\": \"A\", \"intervalMs\": 86400000, \"maxDataPoints\": 1092, \"datasource\":{ \"uid\":\"PD8C576611E62080A\" }, \"rawSql\": \"SELECT 1 as valueOne, 2 as valueTwo\", \"format\": \"table\" } ]",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: ref("github.com/grafana/grafana/pkg/apis/query/v0alpha1.GenericDataQuery"),
},
},
},
},
},
"debug": {
SchemaProps: spec.SchemaProps{
Description: "required: false",
Type: []string{"boolean"},
Format: "",
},
},
},
Required: []string{"queries"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/query/v0alpha1.GenericDataQuery"},
}
}
func schema_pkg_apis_query_v0alpha1_TimeRange(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "TimeRange represents a time range for a query and is a property of DataQuery.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"from": {
SchemaProps: spec.SchemaProps{
Description: "From is the start time of the query.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"to": {
SchemaProps: spec.SchemaProps{
Description: "To is the end time of the query.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"from", "to"},
},
},
}
}
@@ -0,0 +1,5 @@
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/query/v0alpha1,DataSourceApiServer,AliasIDs
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/query/v0alpha1,GenericQueryRequest,Queries
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1,GenericDataQuery,IntervalMS
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1,GenericDataQuery,RefID
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/query/v0alpha1,QueryDataResponse,QueryDataResponse
-51
View File
@@ -1,51 +0,0 @@
# grafana aggregator
The `aggregator` command in this binary is our equivalent of what kube-apiserver does for aggregation using
the `kube-aggregator` pkg. Here, we enable only select controllers that are useful for aggregation in a Grafana
cloud context. In future, Grafana microservices (and even plugins) will run as separate API servers
hosting each their own APIs (with specific Group/Versions). The `aggregator` component here shall act similar to what
`kube-apiserver` does: doing healthchecks for `APIService` objects registered against it and acting as a proxy for
the specified `GroupVersion` therein.
## How to get started
1. Generate the PKI using `openssl` (for development purposes, we will use the CN of `system:masters`):
```shell
./hack/make-aggregator-pki.sh
```
2. Start the aggregator:
```shell
# This will generate the kubeconfig which you can use in the extension apiservers for
# enforcing delegate authnz under $PWD/data/grafana-apiserver/aggregator.kubeconfig
go run ./pkg/cmd/grafana aggregator --secure-port 8443 \
--proxy-client-cert-file $PWD/data/grafana-aggregator/client.crt \
--proxy-client-key-file $PWD/data/grafana-aggregator/client.key
```
3. Apply the manifests:
```shell
export KUBECONFIG=$PWD/data/grafana-apiserver/aggregator.kubeconfig
kubectl apply -k ./pkg/cmd/grafana/apiserver/deploy/aggregator-test
# SAMPLE OUTPUT
# apiservice.apiregistration.k8s.io/v0alpha1.example.grafana.app created
# externalname.service.grafana.app/example-apiserver created
kubectl get apiservice
# SAMPLE OUTPUT
# NAME SERVICE AVAILABLE AGE
# v0alpha1.example.grafana.app grafana/example-apiserver False (FailedDiscoveryCheck) 29m
```
4. In another tab, start the example microservice that will be aggregated by the parent apiserver:
```shell
go run ./pkg/cmd/grafana apiserver example.grafana.app \
--kubeconfig $PWD/data/grafana-aggregator/aggregator.kubeconfig \
--secure-port 7443 \
--client-ca-file=$PWD/data/grafana-aggregator/ca.crt
```
5. Check `APIService` again:
```shell
export KUBECONFIG=$PWD/data/grafana-apiserver/aggregator.kubeconfig
kubectl get apiservice
# SAMPLE OUTPUT
# NAME SERVICE AVAILABLE AGE
# v0alpha1.example.grafana.app grafana/example-apiserver True 30m
```
+46 -103
View File
@@ -1,31 +1,21 @@
package apiserver
import (
"fmt"
"os"
"path"
"strings"
"github.com/spf13/cobra"
genericapifilters "k8s.io/apiserver/pkg/endpoints/filters"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/options"
"k8s.io/apiserver/pkg/util/notfoundhandler"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/component-base/cli"
"k8s.io/klog/v2"
aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"
"github.com/grafana/grafana/pkg/aggregator"
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/utils"
)
const (
aggregatorDataPath = "data"
defaultAggregatorEtcdPathPrefix = "/registry/grafana.aggregator"
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
)
func newCommandStartExampleAPIServer(o *APIServerOptions, stopCh <-chan struct{}) *cobra.Command {
devAcknowledgementNotice := "The apiserver command is in heavy development. The entire setup is subject to change without notice"
runtimeConfig := ""
cmd := &cobra.Command{
Use: "apiserver [api group(s)]",
@@ -34,8 +24,13 @@ func newCommandStartExampleAPIServer(o *APIServerOptions, stopCh <-chan struct{}
devAcknowledgementNotice,
Example: "grafana apiserver example.grafana.app",
RunE: func(c *cobra.Command, args []string) error {
apis, err := readRuntimeConfig(runtimeConfig)
if err != nil {
return err
}
// Load each group from the args
if err := o.loadAPIGroupBuilders(args[1:]); err != nil {
if err := o.loadAPIGroupBuilders(apis); err != nil {
return err
}
@@ -56,6 +51,8 @@ func newCommandStartExampleAPIServer(o *APIServerOptions, stopCh <-chan struct{}
},
}
cmd.Flags().StringVar(&runtimeConfig, "runtime-config", "", "A set of key=value pairs that enable or disable built-in APIs.")
// Register standard k8s flags with the command line
o.RecommendedOptions = options.NewRecommendedOptions(
defaultEtcdPathPrefix,
@@ -75,96 +72,42 @@ func RunCLI() int {
return cli.Run(cmd)
}
func newCommandStartAggregator() *cobra.Command {
devAcknowledgementNotice := "The aggregator command is in heavy development. The entire setup is subject to change without notice"
cwd, err := os.Getwd()
if err != nil {
panic("could not determine current directory")
}
extraConfig := &aggregator.ExtraConfig{
DataPath: path.Join(cwd, aggregatorDataPath),
}
// Register standard k8s flags with the command line
recommendedOptions := options.NewRecommendedOptions(
defaultAggregatorEtcdPathPrefix,
aggregatorscheme.Codecs.LegacyCodec(), // codec is passed to etcd and hence not used
)
cmd := &cobra.Command{
Use: "aggregator",
Short: "Run the grafana aggregator",
Long: "Run a standalone kubernetes based aggregator server. " +
devAcknowledgementNotice,
Example: "grafana aggregator",
RunE: func(c *cobra.Command, args []string) error {
serverOptions, err := aggregator.NewAggregatorServerOptions(os.Stdout, os.Stderr, recommendedOptions, extraConfig)
serverOptions.Config.Complete()
if err != nil {
klog.Errorf("Could not create aggregator server options: %s", err)
os.Exit(1)
}
return run(serverOptions)
},
}
recommendedOptions.AddFlags(cmd.Flags())
extraConfig.AddFlags(cmd.Flags())
return cmd
type apiConfig struct {
group string
version string
enabled bool
}
func run(serverOptions *aggregator.AggregatorServerOptions) error {
if err := serverOptions.LoadAPIGroupBuilders(); err != nil {
klog.Errorf("Error loading prerequisite APIs: %s", err)
return err
}
notFoundHandler := notfoundhandler.New(serverOptions.Config.SharedConfig.Serializer, genericapifilters.NoMuxAndDiscoveryIncompleteKey)
apiExtensionsServer, err := serverOptions.Config.ApiExtensionsComplete.New(genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))
if err != nil {
return err
}
aggregator, err := serverOptions.CreateAggregatorServer(apiExtensionsServer.GenericAPIServer, apiExtensionsServer.Informers)
if err != nil {
klog.Errorf("Error creating aggregator server: %s", err)
return err
}
// Install the API Group+version
err = grafanaapiserver.InstallAPIs(aggregator.GenericAPIServer, serverOptions.Config.Aggregator.GenericConfig.RESTOptionsGetter, serverOptions.Builders)
if err != nil {
klog.Errorf("Error installing apis: %s", err)
return err
}
if err := clientcmd.WriteToFile(
utils.FormatKubeConfig(aggregator.GenericAPIServer.LoopbackClientConfig),
path.Join(aggregatorDataPath, "grafana-aggregator", "aggregator.kubeconfig"),
); err != nil {
klog.Errorf("Error persisting aggregator.kubeconfig: %s", err)
return err
}
prepared, err := aggregator.PrepareRun()
if err != nil {
return err
}
stopCh := genericapiserver.SetupSignalHandler()
if err := prepared.Run(stopCh); err != nil {
return err
}
return nil
func (a apiConfig) String() string {
return fmt.Sprintf("%s/%s=%v", a.group, a.version, a.enabled)
}
func RunCobraWrapper() int {
cmd := newCommandStartAggregator()
return cli.Run(cmd)
// Supported options are:
//
// <group>/<version>=true|false for a specific API group and version (e.g. dashboards.grafana.app/v0alpha1=true)
// api/all=true|false controls all API versions
// api/ga=true|false controls all API versions of the form v[0-9]+
// api/beta=true|false controls all API versions of the form v[0-9]+beta[0-9]+
// api/alpha=true|false controls all API versions of the form v[0-9]+alpha[0-9]+`)
//
// See: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/
func readRuntimeConfig(cfg string) ([]apiConfig, error) {
if cfg == "" {
return nil, fmt.Errorf("missing --runtime-config={apiservers}")
}
parts := strings.Split(cfg, ",")
apis := make([]apiConfig, len(parts))
for i, part := range parts {
idx0 := strings.Index(part, "/")
idx1 := strings.LastIndex(part, "=")
if idx1 < idx0 || idx0 < 0 {
return nil, fmt.Errorf("expected values in the form: group/version=true")
}
apis[i] = apiConfig{
group: part[:idx0],
version: part[idx0+1 : idx1],
enabled: part[idx1+1:] == "true",
}
}
return apis, nil
}
+22
View File
@@ -0,0 +1,22 @@
package apiserver
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestAdminAPIEndpoint(t *testing.T) {
out, err := readRuntimeConfig("all/all=true,dashboards.grafana.app/v0alpha1=false")
require.NoError(t, err)
require.Equal(t, []apiConfig{
{group: "all", version: "all", enabled: true},
{group: "dashboards.grafana.app", version: "v0alpha1", enabled: false},
}, out)
require.Equal(t, "all/all=true", fmt.Sprintf("%v", out[0]))
// Empty is an error
_, err = readRuntimeConfig("")
require.Error(t, err)
}
+34 -15
View File
@@ -14,10 +14,14 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/example"
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
"github.com/grafana/grafana/pkg/registry/apis/query"
"github.com/grafana/grafana/pkg/registry/apis/query/runner"
"github.com/grafana/grafana/pkg/server"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
grafanaAPIServer "github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/featuremgmt"
grafanaAPIServer "github.com/grafana/grafana/pkg/services/grafana-apiserver"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/utils"
"github.com/grafana/grafana/pkg/setting"
)
@@ -28,7 +32,7 @@ const (
// APIServerOptions contains the state for the apiserver
type APIServerOptions struct {
builders []grafanaAPIServer.APIGroupBuilder
builders []builder.APIGroupBuilder
RecommendedOptions *options.RecommendedOptions
AlternateDNS []string
@@ -43,29 +47,44 @@ func newAPIServerOptions(out, errOut io.Writer) *APIServerOptions {
}
}
func (o *APIServerOptions) loadAPIGroupBuilders(args []string) error {
o.builders = []grafanaAPIServer.APIGroupBuilder{}
for _, g := range args {
switch g {
// No dependencies for testing
func (o *APIServerOptions) loadAPIGroupBuilders(runtime []apiConfig) error {
o.builders = []builder.APIGroupBuilder{}
for _, gv := range runtime {
if !gv.enabled {
return fmt.Errorf("disabling apis is not yet supported")
}
switch gv.group {
case "all":
return fmt.Errorf("managing all APIs is not yet supported")
case "example.grafana.app":
o.builders = append(o.builders, example.NewTestingAPIBuilder())
// Only works with testdata
case "query.grafana.app":
o.builders = append(o.builders, query.NewQueryAPIBuilder(
featuremgmt.WithFeatures(),
runner.NewDummyTestRunner(),
runner.NewDummyRegistry(),
))
case "featuretoggle.grafana.app":
features := featuremgmt.WithFeatureManager(setting.FeatureMgmtSettings{}, nil) // none... for now
o.builders = append(o.builders, featuretoggle.NewFeatureFlagAPIBuilder(features))
o.builders = append(o.builders,
featuretoggle.NewFeatureFlagAPIBuilder(
featuremgmt.WithFeatureManager(setting.FeatureMgmtSettings{}, nil), // none... for now
&actest.FakeAccessControl{ExpectedEvaluate: false},
),
)
case "testdata.datasource.grafana.app":
ds, err := server.InitializeDataSourceAPIServer(g)
ds, err := server.InitializeDataSourceAPIServer(gv.group)
if err != nil {
return err
}
o.builders = append(o.builders, ds)
default:
return fmt.Errorf("unknown group: %s", g)
return fmt.Errorf("unsupported runtime-config: %v", gv)
}
}
if len(o.builders) < 1 {
return fmt.Errorf("expected group name(s) in the command line arguments")
return fmt.Errorf("no apis matched ")
}
// Install schemas
@@ -162,7 +181,7 @@ func (o *APIServerOptions) Config() (*genericapiserver.RecommendedConfig, error)
serverConfig.DisabledPostStartHooks = serverConfig.DisabledPostStartHooks.Insert("priority-and-fairness-config-consumer")
// Add OpenAPI specs for each group+version
err := grafanaAPIServer.SetupConfig(serverConfig, o.builders)
err := builder.SetupConfig(grafanaAPIServer.Scheme, serverConfig, o.builders)
return serverConfig, err
}
@@ -190,7 +209,7 @@ func (o *APIServerOptions) RunAPIServer(config *genericapiserver.RecommendedConf
}
// Install the API Group+version
err = grafanaAPIServer.InstallAPIs(server, config.RESTOptionsGetter, o.builders)
err = builder.InstallAPIs(grafanaAPIServer.Scheme, grafanaAPIServer.Codecs, server, config.RESTOptionsGetter, o.builders, true)
if err != nil {
return err
}
-12
View File
@@ -46,18 +46,6 @@ func main() {
},
},
gsrv.ServerCommand(version, commit, enterpriseCommit, buildBranch, buildstamp),
{
// The kube-aggregator inspired grafana aggregator
Name: "aggregator",
Usage: "run grafana aggregator (experimental)",
// Skip parsing flags because the command line is actually managed by cobra
SkipFlagParsing: true,
Action: func(context *cli.Context) error {
// exit here because apiserver handles its own error output
os.Exit(apiserver.RunCobraWrapper())
return nil
},
},
},
CommandNotFound: cmdNotFound,
EnableBashCompletion: true,
@@ -2,6 +2,7 @@ package httpclientprovider
import (
"net/http"
"strconv"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/httpclient"
@@ -18,7 +19,7 @@ var (
Name: "datasource_request_total",
Help: "A counter for outgoing requests for a data source",
},
[]string{"datasource", "datasource_type", "code", "method"},
[]string{"datasource", "datasource_type", "code", "method", "secure_socks_ds_proxy_enabled"},
)
datasourceRequestHistogram = promauto.NewHistogramVec(
@@ -27,7 +28,7 @@ var (
Name: "datasource_request_duration_seconds",
Help: "histogram of durations of outgoing data source requests sent from Grafana",
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 50, 100},
}, []string{"datasource", "datasource_type", "code", "method"},
}, []string{"datasource", "datasource_type", "code", "method", "secure_socks_ds_proxy_enabled"},
)
datasourceResponseHistogram = promauto.NewHistogramVec(
@@ -36,7 +37,7 @@ var (
Name: "datasource_response_size_bytes",
Help: "histogram of data source response sizes returned to Grafana",
Buckets: []float64{128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576},
}, []string{"datasource", "datasource_type"},
}, []string{"datasource", "datasource_type", "secure_socks_ds_proxy_enabled"},
)
datasourceRequestsInFlight = promauto.NewGaugeVec(
@@ -45,7 +46,7 @@ var (
Name: "datasource_request_in_flight",
Help: "A gauge of outgoing data source requests currently being sent by Grafana",
},
[]string{"datasource", "datasource_type"},
[]string{"datasource", "datasource_type", "secure_socks_ds_proxy_enabled"},
)
)
@@ -82,7 +83,11 @@ func DataSourceMetricsMiddleware() sdkhttpclient.Middleware {
return next
}
labels := prometheus.Labels{"datasource": datasourceLabelName, "datasource_type": datasourceLabelType}
labels := prometheus.Labels{
"datasource": datasourceLabelName,
"datasource_type": datasourceLabelType,
"secure_socks_ds_proxy_enabled": strconv.FormatBool(opts.ProxyOptions != nil && opts.ProxyOptions.Enabled),
}
return executeMiddlewareFunc(next, labels)
})
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
)
@@ -103,29 +104,56 @@ func TestDataSourceMetricsMiddleware(t *testing.T) {
executeMiddlewareFunc = origExecuteMiddlewareFunc
})
ctx := &testContext{}
finalRoundTripper := ctx.createRoundTripper("finalrt")
mw := DataSourceMetricsMiddleware()
rt := mw.CreateMiddleware(httpclient.Options{Labels: map[string]string{"datasource_name": "My Data Source 123", "datasource_type": "prometheus"}}, finalRoundTripper)
require.NotNil(t, rt)
middlewareName, ok := mw.(httpclient.MiddlewareName)
require.True(t, ok)
require.Equal(t, DataSourceMetricsMiddlewareName, middlewareName.MiddlewareName())
testCases := []struct {
description string
httpClientOptions httpclient.Options
expectedSecureSocksDSProxyEnabled string
}{
{
description: "secure socks ds proxy is disabled",
httpClientOptions: httpclient.Options{
Labels: map[string]string{"datasource_name": "My Data Source 123", "datasource_type": "prometheus"},
},
expectedSecureSocksDSProxyEnabled: "false",
},
{
description: "secure socks ds proxy is enabled",
httpClientOptions: httpclient.Options{
Labels: map[string]string{"datasource_name": "My Data Source 123", "datasource_type": "prometheus"},
ProxyOptions: &proxy.Options{Enabled: true},
},
expectedSecureSocksDSProxyEnabled: "true",
},
}
req, err := http.NewRequest(http.MethodGet, "http://", nil)
require.NoError(t, err)
res, err := rt.RoundTrip(req)
require.NoError(t, err)
require.NotNil(t, res)
if res.Body != nil {
require.NoError(t, res.Body.Close())
for _, tt := range testCases {
t.Run(tt.description, func(t *testing.T) {
ctx := &testContext{}
finalRoundTripper := ctx.createRoundTripper("finalrt")
mw := DataSourceMetricsMiddleware()
rt := mw.CreateMiddleware(tt.httpClientOptions, finalRoundTripper)
require.NotNil(t, rt)
middlewareName, ok := mw.(httpclient.MiddlewareName)
require.True(t, ok)
require.Equal(t, DataSourceMetricsMiddlewareName, middlewareName.MiddlewareName())
req, err := http.NewRequest(http.MethodGet, "http://", nil)
require.NoError(t, err)
res, err := rt.RoundTrip(req)
require.NoError(t, err)
require.NotNil(t, res)
if res.Body != nil {
require.NoError(t, res.Body.Close())
}
require.Len(t, ctx.callChain, 1)
require.ElementsMatch(t, []string{"finalrt"}, ctx.callChain)
require.True(t, executeMiddlewareCalled)
require.Len(t, labels, 3)
require.Equal(t, "My_Data_Source_123", labels["datasource"])
require.Equal(t, "prometheus", labels["datasource_type"])
require.Equal(t, tt.expectedSecureSocksDSProxyEnabled, labels["secure_socks_ds_proxy_enabled"])
require.True(t, middlewareCalled)
})
}
require.Len(t, ctx.callChain, 1)
require.ElementsMatch(t, []string{"finalrt"}, ctx.callChain)
require.True(t, executeMiddlewareCalled)
require.Len(t, labels, 2)
require.Equal(t, "My_Data_Source_123", labels["datasource"])
require.Equal(t, "prometheus", labels["datasource_type"])
require.True(t, middlewareCalled)
})
}
@@ -1,11 +1,13 @@
package datasource
package plugins
import (
"fmt"
"strings"
)
func getDatasourceGroupNameFromPluginID(pluginId string) (string, error) {
// Get the default API group name for from a plugin ID
// NOTE: this is a work in progress, and may change without notice
func GetDatasourceGroupNameFromPluginID(pluginId string) (string, error) {
if pluginId == "" {
return "", fmt.Errorf("bad pluginID (empty)")
}
@@ -1,4 +1,4 @@
package datasource
package plugins
import (
"testing"
@@ -22,11 +22,11 @@ func TestUtils(t *testing.T) {
}
func getIDIgnoreError(id string) string {
v, _ := getDatasourceGroupNameFromPluginID(id)
v, _ := GetDatasourceGroupNameFromPluginID(id)
return v
}
func getErrorIgnoreValue(id string) error {
_, err := getDatasourceGroupNameFromPluginID(id)
_, err := GetDatasourceGroupNameFromPluginID(id)
return err
}
+4 -1
View File
@@ -48,12 +48,14 @@ type Cfg struct {
AngularSupportEnabled bool
HideAngularDeprecation []string
ConcurrentQueryCount int
}
func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSettings, pluginsAllowUnsigned []string,
awsAllowedAuthProviders []string, awsAssumeRoleEnabled bool, awsExternalId string, azure *azsettings.AzureSettings, secureSocksDSProxy setting.SecureSocksDSProxySettings,
grafanaVersion string, logDatasourceRequests bool, pluginsCDNURLTemplate string, appURL string, appSubURL string, tracing Tracing, features featuremgmt.FeatureToggles, angularSupportEnabled bool,
grafanaComURL string, disablePlugins []string, hideAngularDeprecation []string, forwardHostEnvVars []string) *Cfg {
grafanaComURL string, disablePlugins []string, hideAngularDeprecation []string, forwardHostEnvVars []string, concurrentQueryCount int) *Cfg {
return &Cfg{
log: log.New("plugin.cfg"),
PluginsPath: pluginsPath,
@@ -77,5 +79,6 @@ func NewCfg(devMode bool, pluginsPath string, pluginSettings setting.PluginSetti
AngularSupportEnabled: angularSupportEnabled,
HideAngularDeprecation: hideAngularDeprecation,
ForwardHostEnvVars: forwardHostEnvVars,
ConcurrentQueryCount: concurrentQueryCount,
}
}
+15 -11
View File
@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
"github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/auth"
"github.com/grafana/grafana/pkg/plugins/config"
@@ -105,14 +106,17 @@ func (s *Service) GetConfigMap(ctx context.Context, pluginID string, _ *auth.Ext
if s.cfg.GrafanaAppURL != "" {
m[backend.AppURL] = s.cfg.GrafanaAppURL
}
if s.cfg.ConcurrentQueryCount != 0 {
m[backend.ConcurrentQueryCount] = strconv.Itoa(s.cfg.ConcurrentQueryCount)
}
// TODO add support via plugin SDK
//if externalService != nil {
// if externalService != nil {
// m[oauthtokenretriever.AppURL] = s.cfg.GrafanaAppURL
// m[oauthtokenretriever.AppClientID] = externalService.ClientID
// m[oauthtokenretriever.AppClientSecret] = externalService.ClientSecret
// m[oauthtokenretriever.AppPrivateKey] = externalService.PrivateKey
//}
// }
if s.cfg.Features != nil {
enabledFeatures := s.cfg.Features.GetEnabled(ctx)
@@ -126,15 +130,15 @@ func (s *Service) GetConfigMap(ctx context.Context, pluginID string, _ *auth.Ext
}
}
// TODO add support via plugin SDK
//if s.cfg.AWSAssumeRoleEnabled {
// if s.cfg.AWSAssumeRoleEnabled {
// m[awsds.AssumeRoleEnabledEnvVarKeyName] = "true"
//}
//if len(s.cfg.AWSAllowedAuthProviders) > 0 {
// }
// if len(s.cfg.AWSAllowedAuthProviders) > 0 {
// m[awsds.AllowedAuthProvidersEnvVarKeyName] = strings.Join(s.cfg.AWSAllowedAuthProviders, ",")
//}
//if s.cfg.AWSExternalId != "" {
// }
// if s.cfg.AWSExternalId != "" {
// m[awsds.GrafanaAssumeRoleExternalIdKeyName] = s.cfg.AWSExternalId
//}
// }
if s.cfg.ProxySettings.Enabled {
m[proxy.PluginSecureSocksProxyEnabled] = "true"
@@ -198,10 +202,10 @@ func (s *Service) GetConfigMap(ctx context.Context, pluginID string, _ *auth.Ext
}
// TODO add support via plugin SDK
//ps := getPluginSettings(pluginID, s.cfg)
//for k, v := range ps {
// ps := getPluginSettings(pluginID, s.cfg)
// for k, v := range ps {
// m[fmt.Sprintf("%s_%s", customConfigPrefix, strings.ToUpper(k))] = v
//}
// }
return m
}
+27
View File
@@ -798,6 +798,33 @@ func TestService_GetConfigMap_appURL(t *testing.T) {
})
}
func TestService_GetConfigMap_concurrentQueryCount(t *testing.T) {
t.Run("Uses the configured concurrent query count", func(t *testing.T) {
s := &Service{
cfg: &config.Cfg{
ConcurrentQueryCount: 42,
},
}
require.Equal(t, map[string]string{"GF_CONCURRENT_QUERY_COUNT": "42"}, s.GetConfigMap(context.Background(), "", nil))
})
t.Run("Doesn't set the concurrent query count if it is not in the config", func(t *testing.T) {
s := &Service{
cfg: &config.Cfg{},
}
require.Equal(t, map[string]string{}, s.GetConfigMap(context.Background(), "", nil))
})
t.Run("Doesn't set the concurrent query count if it is zero", func(t *testing.T) {
s := &Service{
cfg: &config.Cfg{
ConcurrentQueryCount: 0,
},
}
require.Equal(t, map[string]string{}, s.GetConfigMap(context.Background(), "", nil))
})
}
func TestService_GetConfigMap_azure(t *testing.T) {
azSettings := &azsettings.AzureSettings{
Cloud: azsettings.AzurePublic,
@@ -34,7 +34,7 @@ func corePlugins(rt *thema.Runtime) []pfs.ParsedPlugin {
return []pfs.ParsedPlugin{
parsePluginOrPanic("public/app/plugins/datasource/alertmanager", "alertmanager", rt),
parsePluginOrPanic("public/app/plugins/datasource/azuremonitor", "grafana_azure_monitor_datasource", rt),
parsePluginOrPanic("public/app/plugins/datasource/cloud-monitoring", "grafana_cloud_monitoring_datasource", rt),
parsePluginOrPanic("public/app/plugins/datasource/cloud-monitoring", "stackdriver", rt),
parsePluginOrPanic("public/app/plugins/datasource/cloudwatch", "cloudwatch", rt),
parsePluginOrPanic("public/app/plugins/datasource/dashboard", "dashboard", rt),
parsePluginOrPanic("public/app/plugins/datasource/elasticsearch", "elasticsearch", rt),
+6
View File
@@ -5,11 +5,14 @@ import (
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/registry/apis/example"
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
"github.com/grafana/grafana/pkg/registry/apis/folders"
"github.com/grafana/grafana/pkg/registry/apis/playlist"
"github.com/grafana/grafana/pkg/registry/apis/query"
"github.com/grafana/grafana/pkg/registry/apis/service"
)
var (
@@ -24,9 +27,12 @@ func ProvideRegistryServiceSink(
_ *dashboard.DashboardsAPIBuilder,
_ *playlist.PlaylistAPIBuilder,
_ *example.TestingAPIBuilder,
_ *dashboardsnapshot.SnapshotsAPIBuilder,
_ *featuretoggle.FeatureFlagAPIBuilder,
_ *datasource.DataSourceAPIBuilder,
_ *folders.FolderAPIBuilder,
_ *service.ServiceAPIBuilder,
_ *query.QueryAPIBuilder,
) *Service {
return &Service{}
}

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