mirror of
https://github.com/grafana/grafana.git
synced 2026-01-06 17:33:49 +08:00
Compare commits
30 Commits
release-12
...
v12.0.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a909c838b5 | ||
|
|
8e1063cf0a | ||
|
|
26c1134326 | ||
|
|
31c4188746 | ||
|
|
da3717b315 | ||
|
|
3c845fd337 | ||
|
|
af54287df1 | ||
|
|
560be84dc8 | ||
|
|
6b87718cde | ||
|
|
0e620047b5 | ||
|
|
c0e87eeb98 | ||
|
|
550a339cab | ||
|
|
27a095a971 | ||
|
|
394f5d8ce4 | ||
|
|
323a4a24fa | ||
|
|
cd1a52454f | ||
|
|
58dd6d242a | ||
|
|
0d2ee90ff1 | ||
|
|
39ca690c5a | ||
|
|
1bac5badac | ||
|
|
b0356011e3 | ||
|
|
bcc78ed4eb | ||
|
|
cc150e5bb3 | ||
|
|
b53d333cbf | ||
|
|
5ac3d0c76a | ||
|
|
cd1e08734c | ||
|
|
5195a6d72e | ||
|
|
ab1e43b8b9 | ||
|
|
560f875837 | ||
|
|
791f3f3c1d |
@@ -65,7 +65,7 @@ require (
|
||||
github.com/go-toolsmith/astp v1.1.0 // indirect
|
||||
github.com/go-toolsmith/strparse v1.1.0 // indirect
|
||||
github.com/go-toolsmith/typep v1.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
|
||||
@@ -142,8 +142,8 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi
|
||||
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
|
||||
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
|
||||
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
|
||||
@@ -18,7 +18,7 @@ require (
|
||||
github.com/evilmartians/lefthook v1.4.8 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
|
||||
@@ -29,8 +29,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
|
||||
@@ -24,7 +24,7 @@ require (
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-openapi/validate v0.24.0 // indirect
|
||||
github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.2 // indirect
|
||||
|
||||
@@ -41,8 +41,8 @@ github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3Bum
|
||||
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
||||
github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37 h1:KFcZmKdZmapAog2+eL1buervAYrYolBZk7fMecPPDmo=
|
||||
github.com/go-swagger/go-swagger v0.30.6-0.20240310114303-db51e79a0e37/go.mod h1:i1/E+d8iPNReSE7y04FaVu5OPKB3il5cn+T1Egogg3I=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
|
||||
1
.github/actions/build-package/action.yml
vendored
1
.github/actions/build-package/action.yml
vendored
@@ -139,6 +139,7 @@ runs:
|
||||
with:
|
||||
verb: run
|
||||
dagger-flags: --verbose=0
|
||||
version: 0.18.8
|
||||
args: go run -C ${GRAFANA_PATH} ./pkg/build/cmd artifacts --artifacts ${ARTIFACTS} --grafana-dir=${GRAFANA_PATH} --alpine-base=${ALPINE_BASE} --ubuntu-base=${UBUNTU_BASE} --enterprise-dir=${ENTERPRISE_PATH} --version=${VERSION} --patches-repo=${PATCHES_REPO} --patches-ref=${PATCHES_REF} --patches-path=${PATCHES_PATH} --build-id=${BUILD_ID} --tag-format="${TAG_FORMAT}" --ubuntu-tag-format="${UBUNTU_TAG_FORMAT}" --org=${DOCKER_ORG} --registry=${DOCKER_REGISTRY} --checksum=${CHECKSUM} --verify=${VERIFY} > $OUTFILE
|
||||
- id: output
|
||||
shell: bash
|
||||
|
||||
28
.github/workflows/bump-version.yml
vendored
28
.github/workflows/bump-version.yml
vendored
@@ -13,17 +13,29 @@ on:
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
bump-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Grafana
|
||||
uses: actions/checkout@v4
|
||||
- uses: grafana/shared-workflows/actions/get-vault-secrets@main
|
||||
with:
|
||||
persist-credentials: false
|
||||
repo_secrets: |
|
||||
GRAFANA_DELIVERY_BOT_APP_PEM=delivery-bot-app:PRIVATE_KEY
|
||||
- name: Generate token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
|
||||
with:
|
||||
app_id: ${{ vars.DELIVERY_BOT_APP_ID }}
|
||||
private_key: ${{ env.GRAFANA_DELIVERY_BOT_APP_PEM }}
|
||||
repositories: '["grafana"]'
|
||||
permissions: '{"contents": "write", "pull_requests": "write", "workflows": "write"}'
|
||||
- name: Checkout Grafana
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Update package.json versions
|
||||
uses: ./pkg/build/actions/bump-version
|
||||
with:
|
||||
@@ -35,10 +47,10 @@ jobs:
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
run: |
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "grafana-delivery-bot[bot]"
|
||||
git config --local user.email "grafana-delivery-bot[bot]@users.noreply.github.com"
|
||||
git config --local --add --bool push.autoSetupRemote true
|
||||
git checkout -b "bump-version/${RUN_ID}/${VERSION}"
|
||||
git add .
|
||||
|
||||
4
.github/workflows/pr-e2e-tests.yml
vendored
4
.github/workflows/pr-e2e-tests.yml
vendored
@@ -48,6 +48,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
- uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
|
||||
with:
|
||||
version: 0.18.8
|
||||
verb: run
|
||||
args: go -C grafana run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}/grafana" > out.txt
|
||||
- run: mv "$(cat out.txt)" grafana.tar.gz
|
||||
@@ -140,6 +141,7 @@ jobs:
|
||||
- name: Run E2E tests
|
||||
uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
|
||||
with:
|
||||
version: 0.18.8
|
||||
verb: run
|
||||
args: go run ./pkg/build/e2e --package=grafana.tar.gz
|
||||
--suite=${{ matrix.path }}
|
||||
@@ -178,12 +180,14 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
|
||||
with:
|
||||
version: 0.18.8
|
||||
verb: run
|
||||
args: go run ./pkg/build/a11y --package=grafana.tar.gz
|
||||
- name: Run non-PR a11y test
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
|
||||
with:
|
||||
version: 0.18.8
|
||||
verb: run
|
||||
args: go run ./pkg/build/a11y --package=grafana.tar.gz --no-threshold-fail
|
||||
|
||||
|
||||
154
.github/workflows/release-build.yml
vendored
154
.github/workflows/release-build.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
schedule:
|
||||
# Every weeknight at midnight
|
||||
# "Scheduled workflows will only run on the default branch." (docs.github.com)
|
||||
- cron: "0 0 * * 1-5"
|
||||
- cron: '0 0 * * 1-5'
|
||||
push:
|
||||
branches:
|
||||
- release-*.*.*
|
||||
@@ -49,14 +49,14 @@ jobs:
|
||||
setup:
|
||||
name: setup
|
||||
runs-on: github-hosted-ubuntu-x64-small
|
||||
if: github.repository == 'grafana/grafana'
|
||||
if: (github.repository == 'grafana/grafana') || (github.repository == 'grafana/grafana-security-mirror' && contains(github.ref_name, '+security'))
|
||||
outputs:
|
||||
version: ${{ steps.output.outputs.version }}
|
||||
grafana-commit: ${{ steps.output.outputs.grafana_commit }}
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up version (Release Branches)
|
||||
@@ -103,11 +103,13 @@ jobs:
|
||||
BUILD_ID: ${{ github.run_id }}
|
||||
BUCKET: grafana-prerelease
|
||||
GRAFANA_COMMIT: ${{ needs.setup.outputs.grafana-commit }}
|
||||
SOURCE_EVENT: ${{ inputs.source-event || github.event_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
script: |
|
||||
const {REF, VERSION, BUILD_ID, BUCKET, GRAFANA_COMMIT, GITHUB_EVENT_NAME} = process.env;
|
||||
|
||||
const {REF, VERSION, BUILD_ID, BUCKET, GRAFANA_COMMIT, SOURCE_EVENT, REPO} = process.env;
|
||||
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'grafana',
|
||||
repo: 'grafana-enterprise',
|
||||
@@ -118,7 +120,8 @@ jobs:
|
||||
"build-id": String(BUILD_ID),
|
||||
"bucket": BUCKET,
|
||||
"grafana-commit": GRAFANA_COMMIT,
|
||||
"source-event": GITHUB_EVENT_NAME,
|
||||
"source-event": SOURCE_EVENT,
|
||||
"upstream": REPO,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -137,7 +140,7 @@ jobs:
|
||||
# The downside to this is that the frontend will be built for each one when it could be reused for all of them.
|
||||
# This could be a future improvement.
|
||||
include:
|
||||
- name: linux-amd64
|
||||
- name: linux-amd64 # publish-npm relies on this step building npm packages
|
||||
artifacts: targz:grafana:linux/amd64,deb:grafana:linux/amd64,rpm:grafana:linux/amd64,docker:grafana:linux/amd64,docker:grafana:linux/amd64:ubuntu,npm:grafana,storybook
|
||||
verify: true
|
||||
- name: linux-arm64
|
||||
@@ -165,8 +168,8 @@ jobs:
|
||||
artifacts: targz:grafana:darwin/arm64
|
||||
verify: true
|
||||
steps:
|
||||
- uses: grafana/shared-workflows/actions/dockerhub-login@main
|
||||
- uses: actions/checkout@v4
|
||||
- uses: grafana/shared-workflows/actions/dockerhub-login@dockerhub-login/v1.0.2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up QEMU
|
||||
@@ -194,6 +197,7 @@ jobs:
|
||||
name: artifacts-${{ matrix.name }}
|
||||
path: ${{ steps.build.outputs.dist-dir }}
|
||||
retention-days: 1
|
||||
|
||||
publish-artifacts:
|
||||
name: Upload artifacts
|
||||
uses: grafana/grafana/.github/workflows/publish-artifact.yml@main
|
||||
@@ -208,3 +212,135 @@ jobs:
|
||||
run-id: ${{ github.run_id }}
|
||||
bucket-path: ${{ needs.setup.outputs.version }}_${{ github.run_id }}
|
||||
environment: prod
|
||||
|
||||
publish-dockerhub:
|
||||
if: github.ref_name == 'main'
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
runs-on: ubuntu-x64-small
|
||||
needs:
|
||||
- setup
|
||||
- build
|
||||
steps:
|
||||
- uses: grafana/shared-workflows/actions/dockerhub-login@dockerhub-login/v1.0.2
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
name: artifacts-list-linux-amd64
|
||||
path: .
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
name: artifacts-list-linux-arm64
|
||||
path: .
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
name: artifacts-list-linux-armv7
|
||||
path: .
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
name: artifacts-linux-amd64
|
||||
path: dist
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
name: artifacts-linux-arm64
|
||||
path: dist
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
name: artifacts-linux-armv7
|
||||
path: dist
|
||||
- name: Push to Docker Hub
|
||||
env:
|
||||
VERSION: ${{ needs.setup.outputs.version }}
|
||||
run: |
|
||||
# grep can use a wildcard but then it includes the filename as part of the result and that gets complicated.
|
||||
# It's easier to use cat to combine the artifact lists
|
||||
cat artifacts-*.txt > artifacts.txt
|
||||
grep 'grafana_.*docker.tar.gz$' artifacts.txt | xargs -I % docker load -i % | sed 's/Loaded image: //g' | tee docker_images
|
||||
while read -r line; do
|
||||
# This tag will be `grafana/grafana-image-tags:...`
|
||||
docker push "$line"
|
||||
done < docker_images
|
||||
|
||||
docker manifest create grafana/grafana:main "grafana/grafana-image-tags:${VERSION}-amd64" "grafana/grafana-image-tags:${VERSION}-arm64" "grafana/grafana-image-tags:${VERSION}-armv7"
|
||||
docker manifest create grafana/grafana:main-ubuntu "grafana/grafana-image-tags:${VERSION}-ubuntu-amd64" "grafana/grafana-image-tags:${VERSION}-ubuntu-arm64" "grafana/grafana-image-tags:${VERSION}-ubuntu-armv7"
|
||||
docker manifest create "grafana/grafana-dev:${VERSION}" "grafana/grafana-image-tags:${VERSION}-amd64" "grafana/grafana-image-tags:${VERSION}-arm64" "grafana/grafana-image-tags:${VERSION}-armv7"
|
||||
docker manifest create "grafana/grafana-dev:${VERSION}-ubuntu" "grafana/grafana-image-tags:${VERSION}-ubuntu-amd64" "grafana/grafana-image-tags:${VERSION}-ubuntu-arm64" "grafana/grafana-image-tags:${VERSION}-ubuntu-armv7"
|
||||
|
||||
docker manifest push grafana/grafana:main
|
||||
docker manifest push grafana/grafana:main-ubuntu
|
||||
docker manifest push "grafana/grafana-dev:${VERSION}"
|
||||
docker manifest push "grafana/grafana-dev:${VERSION}-ubuntu"
|
||||
|
||||
dispatch-npm-canaries:
|
||||
if: github.ref_name == 'main'
|
||||
name: Dispatch publish NPM canaries
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
runs-on: ubuntu-x64-small
|
||||
needs:
|
||||
- setup
|
||||
steps:
|
||||
- name: Dispatch action
|
||||
env:
|
||||
GRAFANA_COMMIT: ${{ needs.setup.outputs.grafana-commit }}
|
||||
VERSION: ${{ needs.setup.outputs.version }}
|
||||
BUILD_ID: ${{ github.run_id }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh workflow run release-npm.yml \
|
||||
--repo grafana/grafana \
|
||||
--ref main \
|
||||
--field grafana_commit="$GRAFANA_COMMIT" \
|
||||
--field version="$VERSION" \
|
||||
--field build_id="$BUILD_ID"\
|
||||
--field version_type="canary"
|
||||
|
||||
# notify-pr creates (or updates) a comment in a pull request to link to this workflow where the release artifacts are
|
||||
# being built.
|
||||
notify-pr:
|
||||
runs-on: ubuntu-x64-small
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
needs:
|
||||
- setup
|
||||
steps:
|
||||
- id: vault-secrets
|
||||
uses: grafana/shared-workflows/actions/get-vault-secrets@main
|
||||
with:
|
||||
repo_secrets: |
|
||||
GRAFANA_DELIVERY_BOT_APP_PEM=delivery-bot-app:PRIVATE_KEY
|
||||
- name: Generate token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
|
||||
with:
|
||||
app_id: ${{ vars.DELIVERY_BOT_APP_ID }}
|
||||
private_key: ${{ env.GRAFANA_DELIVERY_BOT_APP_PEM }}
|
||||
repositories: '["grafana"]'
|
||||
permissions: '{"issues": "write", "pull_requests": "write", "contents": "read"}'
|
||||
- name: Find PR
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
GRAFANA_COMMIT: ${{ needs.setup.outputs.grafana-commit }}
|
||||
run: echo "ISSUE_NUMBER=$(gh api "/repos/grafana/grafana/commits/${GRAFANA_COMMIT}/pulls" | jq -r '.[0].number')" >> "$GITHUB_ENV"
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ env.ISSUE_NUMBER }}
|
||||
comment-author: 'grafana-delivery-bot[bot]'
|
||||
body-includes: GitHub Actions Build
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Create or update comment
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
issue-number: ${{ env.ISSUE_NUMBER }}
|
||||
body: |
|
||||
:rocket: Your submission is now being built and packaged.
|
||||
|
||||
- [GitHub Actions Build](https://github.com/grafana/grafana/actions/runs/${{ github.run_id }})
|
||||
- Version: ${{ needs.setup.outputs.version }}
|
||||
edit-mode: replace
|
||||
|
||||
147
.github/workflows/release-npm.yml
vendored
Normal file
147
.github/workflows/release-npm.yml
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
name: Release NPM packages
|
||||
run-name: Publish NPM ${{ inputs.version_type }} ${{ inputs.version }}
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
grafana_commit:
|
||||
description: 'Grafana commit SHA to build against'
|
||||
required: true
|
||||
type: string
|
||||
version:
|
||||
description: 'Version to publish as'
|
||||
required: true
|
||||
type: string
|
||||
build_id:
|
||||
description: 'Run ID from the original release-build workflow'
|
||||
required: true
|
||||
type: string
|
||||
version_type:
|
||||
description: 'Version type (canary, nightly, stable)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
grafana_commit:
|
||||
description: 'Grafana commit SHA to build against'
|
||||
required: true
|
||||
version:
|
||||
description: 'Version to publish as'
|
||||
required: true
|
||||
build_id:
|
||||
description: 'Run ID from the original release-build workflow'
|
||||
required: true
|
||||
version_type:
|
||||
description: 'Version type (canary, nightly, stable)'
|
||||
required: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
# If called with version_type 'canary' or 'stable', build + publish to NPM
|
||||
# If called with version_type 'nightly', just tag the given version with nightly tag. It was already published by the canary build.
|
||||
|
||||
publish:
|
||||
name: Publish NPM packages
|
||||
runs-on: github-hosted-ubuntu-x64-small
|
||||
if: inputs.version_type == 'canary' || inputs.version_type == 'stable'
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Info
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
GRAFANA_COMMIT: ${{ inputs.grafana_commit }}
|
||||
run: |
|
||||
echo "GRAFANA_COMMIT: $GRAFANA_COMMIT"
|
||||
echo "github.ref: $GITHUB_REF"
|
||||
|
||||
- name: Checkout workflow ref
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 100
|
||||
fetch-tags: false
|
||||
|
||||
# this will fail with "{commit} is not a valid commit" if the commit is valid but
|
||||
# not in the last 100 commits.
|
||||
- name: Verify commit is in workflow HEAD
|
||||
env:
|
||||
GIT_COMMIT: ${{ inputs.grafana_commit }}
|
||||
run: ./.github/workflows/scripts/validate-commit-in-head.sh
|
||||
shell: bash
|
||||
|
||||
- name: Map version type to NPM tag
|
||||
id: npm-tag
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
VERSION_TYPE: ${{ inputs.version_type }}
|
||||
REFERENCE_PKG: "@grafana/runtime"
|
||||
run: |
|
||||
TAG=$(./.github/workflows/scripts/determine-npm-tag.sh)
|
||||
echo "NPM_TAG=$TAG" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
|
||||
- name: Checkout build commit
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.grafana_commit }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
# Trusted Publishing is only available in npm v11.5.1 and later
|
||||
- name: Update npm
|
||||
run: npm install -g npm@^11.5.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Typecheck packages
|
||||
run: yarn run packages:typecheck
|
||||
|
||||
- name: Version, build, and pack packages
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
yarn run packages:build
|
||||
yarn lerna version "$VERSION" \
|
||||
--exact \
|
||||
--no-git-tag-version \
|
||||
--no-push \
|
||||
--force-publish \
|
||||
--yes
|
||||
yarn run packages:pack
|
||||
|
||||
- name: Debug packed files
|
||||
run: tree -a ./npm-artifacts
|
||||
|
||||
- name: Validate packages
|
||||
run: ./scripts/validate-npm-packages.sh
|
||||
|
||||
- name: Debug OIDC Claims
|
||||
uses: github/actions-oidc-debugger@2e9ba5d3f4bebaad1f91a2cede055115738b7ae8
|
||||
with:
|
||||
audience: '${{ github.server_url }}/${{ github.repository_owner }}'
|
||||
|
||||
- name: Publish packages
|
||||
env:
|
||||
NPM_TAG: ${{ steps.npm-tag.outputs.NPM_TAG }}
|
||||
run: ./scripts/publish-npm-packages.sh --dist-tag "$NPM_TAG" --registry 'https://registry.npmjs.org/'
|
||||
|
||||
# TODO: finish this step
|
||||
tag-nightly:
|
||||
name: Tag nightly release
|
||||
runs-on: github-hosted-ubuntu-x64-small
|
||||
if: inputs.version_type == 'nightly'
|
||||
|
||||
steps:
|
||||
- name: Checkout workflow ref
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# TODO: tag the given release with nightly
|
||||
|
||||
1
.github/workflows/release-pr.yml
vendored
1
.github/workflows/release-pr.yml
vendored
@@ -198,6 +198,7 @@ jobs:
|
||||
if: ${{ inputs.bump == true || inputs.bump == 'true' }}
|
||||
uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
|
||||
with:
|
||||
version: 0.18.8
|
||||
verb: run
|
||||
args: go run -C .grafana-main ./pkg/build/actions/bump-version -version="patch"
|
||||
|
||||
|
||||
66
.github/workflows/scripts/determine-npm-tag.sh
vendored
Executable file
66
.github/workflows/scripts/determine-npm-tag.sh
vendored
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
fail() { echo "Error: $*" >&2; exit 1; }
|
||||
|
||||
# Ensure required variables are set
|
||||
if [[ -z "${REFERENCE_PKG}" || -z "${VERSION_TYPE}" || -z "${VERSION}" ]]; then
|
||||
fail "Missing required environment variables: REFERENCE_PKG, VERSION_TYPE, VERSION"
|
||||
fi
|
||||
|
||||
semver_cmp () {
|
||||
IFS='.' read -r -a arr_a <<< "$1"
|
||||
IFS='.' read -r -a arr_b <<< "$2"
|
||||
|
||||
for i in 0 1 2; do
|
||||
local aa=${arr_a[i]:-0}
|
||||
local bb=${arr_b[i]:-0}
|
||||
# shellcheck disable=SC2004
|
||||
if (( 10#$aa > 10#$bb )); then echo gt; return 0; fi
|
||||
if (( 10#$aa < 10#$bb )); then echo lt; return 0; fi
|
||||
done
|
||||
|
||||
echo "eq"
|
||||
}
|
||||
|
||||
|
||||
STABLE_REGEX='^([0-9]+)\.([0-9]+)\.([0-9]+)$' # x.y.z
|
||||
PRE_REGEX='^([0-9]+)\.([0-9]+)\.([0-9]+)-([0-9]+)$' # x.y.z-123456
|
||||
|
||||
# Validate that the VERSION matches VERSION_TYPE
|
||||
# - stable must be x.y.z
|
||||
# - nightly/canary must be x.y.z-123456
|
||||
case "$VERSION_TYPE" in
|
||||
stable)
|
||||
[[ $VERSION =~ $STABLE_REGEX ]] || fail "For 'stable', version must match x.y.z" ;;
|
||||
nightly|canary)
|
||||
[[ $VERSION =~ $PRE_REGEX ]] || fail "For '$VERSION_TYPE', version must match x.y.z-123456" ;;
|
||||
*)
|
||||
fail "Unknown version_type '$VERSION_TYPE'" ;;
|
||||
esac
|
||||
|
||||
# Extract major, minor from VERSION
|
||||
IFS=.- read -r major minor patch _ <<< "$VERSION"
|
||||
|
||||
# Determine NPM tag
|
||||
case "$VERSION_TYPE" in
|
||||
canary) TAG="canary" ;;
|
||||
nightly) TAG="nightly" ;;
|
||||
stable)
|
||||
# Use npm dist-tag "latest" as the reference
|
||||
LATEST="$(npm view --silent "$REFERENCE_PKG" dist-tags.latest 2>/dev/null || true)"
|
||||
echo "Latest for $REFERENCE_PKG is ${LATEST:-<none>}" >&2
|
||||
|
||||
if [[ -z ${LATEST:-} ]]; then
|
||||
TAG="latest" # first ever publish
|
||||
else
|
||||
case "$(semver_cmp "$VERSION" "$LATEST")" in
|
||||
gt) TAG="latest" ;; # newer than reference -> latest
|
||||
lt|eq) TAG="v${major}.${minor}-latest" ;; # older or equal -> vX.Y-latest
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Resolved NPM_TAG=$TAG (VERSION=$VERSION, current latest=${LATEST:-none})" 1>&2 # stderr
|
||||
printf '%s' "$TAG"
|
||||
14
.github/workflows/scripts/validate-commit-in-head.sh
vendored
Executable file
14
.github/workflows/scripts/validate-commit-in-head.sh
vendored
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -z "${GIT_COMMIT:-}" ]]; then
|
||||
echo "Error: Environment variable GIT_COMMIT is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git merge-base --is-ancestor "$GIT_COMMIT" HEAD; then
|
||||
echo "Commit $GIT_COMMIT is contained in HEAD"
|
||||
else
|
||||
echo "Error: Commit $GIT_COMMIT is not contained in HEAD"
|
||||
exit 1
|
||||
fi
|
||||
@@ -24,6 +24,7 @@ require (
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
|
||||
@@ -35,6 +35,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
|
||||
@@ -208,6 +208,6 @@ The above configuration produces the following result in the Time series panel:
|
||||
|
||||
{{< figure src="/media/docs/grafana/screenshot-grafana-10-0-timeseries-time-regions.png" max-width="600px" alt="Time series visualization with time regions business hours" >}}
|
||||
|
||||
Toggle the **Advanced** switch and use [Cron syntax](https://crontab.run/) to set more granular time region controls. The following example sets a time region of 9:00 AM, Monday to Friday:
|
||||
Toggle the **Advanced** switch and use [Cron syntax](https://en.wikipedia.org/wiki/Cron) to set more granular time region controls. The following example sets a time region of 9:00 AM, Monday to Friday:
|
||||
|
||||
{{< figure src="/media/docs/grafana/dashboards/screenshot-annotations-cron-option-v11.6.png" max-width="600px" alt="Time region query with cron syntax" >}}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
labels:
|
||||
products:
|
||||
- cloud
|
||||
- enterprise
|
||||
- oss
|
||||
stage:
|
||||
- experimental
|
||||
@@ -161,6 +160,14 @@ To create a dashboard, follow these steps:
|
||||
1. When you've saved all the changes you want to make to the dashboard, click **Back to dashboard**.
|
||||
1. Toggle off the edit mode switch.
|
||||
|
||||
{{< admonition type="caution" >}}
|
||||
|
||||
Dynamic dashboards is an [experimental](https://grafana.com/docs/release-life-cycle/) feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. To get early access to this feature, request it through [this form](https://docs.google.com/forms/d/e/1FAIpQLSd73nQzuhzcHJOrLFK4ef_uMxHAQiPQh1-rsQUT2MRqbeMLpg/viewform?usp=dialog).
|
||||
|
||||
**Do not enable this feature in production environments as it may result in the irreversible loss of data.**
|
||||
|
||||
{{< /admonition >}}
|
||||
|
||||
## Group panels
|
||||
|
||||
To help create meaningful sections in your dashboard, you can group panels into rows or tabs.
|
||||
@@ -294,6 +301,14 @@ To configure show/hide rules, follow these steps:
|
||||
1. Click **Save**.
|
||||
1. Toggle off the edit mode switch.
|
||||
|
||||
{{< admonition type="caution" >}}
|
||||
|
||||
Dynamic dashboards is an [experimental](https://grafana.com/docs/release-life-cycle/) feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. To get early access to this feature, request it through [this form](https://docs.google.com/forms/d/e/1FAIpQLSd73nQzuhzcHJOrLFK4ef_uMxHAQiPQh1-rsQUT2MRqbeMLpg/viewform?usp=dialog).
|
||||
|
||||
**Do not enable this feature in production environments as it may result in the irreversible loss of data.**
|
||||
|
||||
{{< /admonition >}}
|
||||
|
||||
## Edit dashboards
|
||||
|
||||
When the dashboard is in edit mode, the edit pane that opens displays options associated with the part of the dashboard that it's in focus.
|
||||
@@ -397,3 +412,11 @@ To make a copy of a dashboard, follow these steps:
|
||||
By default, the copied dashboard has the same name as the original dashboard with the word "Copy" appended and is in the same folder.
|
||||
|
||||
1. Click **Save**.
|
||||
|
||||
{{< admonition type="caution" >}}
|
||||
|
||||
Dynamic dashboards is an [experimental](https://grafana.com/docs/release-life-cycle/) feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. To get early access to this feature, request it through [this form](https://docs.google.com/forms/d/e/1FAIpQLSd73nQzuhzcHJOrLFK4ef_uMxHAQiPQh1-rsQUT2MRqbeMLpg/viewform?usp=dialog).
|
||||
|
||||
**Do not enable this feature in production environments as it may result in the irreversible loss of data.**
|
||||
|
||||
{{< /admonition >}}
|
||||
|
||||
@@ -51,7 +51,7 @@ yarn add @grafana/grafana-foundation-sdk
|
||||
For Go, install the SDK package via `go get`:
|
||||
|
||||
```go
|
||||
go get github.com/grafana/grafana-foundation-sdk/go
|
||||
go get github.com/grafana/grafana-foundation-sdk/go@next+cog-v0.0.x
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
@@ -198,6 +198,14 @@ List of enabled loggers.
|
||||
|
||||
Keep dashboard content in the logs (request or response fields). This can significantly increase the size of your logs.
|
||||
|
||||
### log_datasource_query_request_body
|
||||
|
||||
Whether to record data source queries' request body. This can significantly increase the size of your logs. Enabled by default.
|
||||
|
||||
### log_datasource_query_response_body
|
||||
|
||||
Whether to record data source queries' response body. This can significantly increase the size of your logs. Enabled by default.
|
||||
|
||||
### verbose
|
||||
|
||||
Log all requests and keep requests and responses body. This can significantly increase the size of your logs.
|
||||
|
||||
@@ -373,6 +373,10 @@ enabled = false
|
||||
loggers = file
|
||||
# Keep dashboard content in the logs (request or response fields); this can significantly increase the size of your logs.
|
||||
log_dashboard_content = false
|
||||
# Whether to record data source queries' request body. This can significantly increase the size of your logs. Enabled by default.
|
||||
log_datasource_query_request_body = true
|
||||
# Whether to record data source queries' response body. This can significantly increase the size of your logs. Enabled by default.
|
||||
log_datasource_query_response_body = true
|
||||
# Keep requests and responses body; this can significantly increase the size of your logs.
|
||||
verbose = false
|
||||
# Write an audit log for every status code.
|
||||
|
||||
42
go.mod
42
go.mod
@@ -77,7 +77,7 @@ require (
|
||||
github.com/googleapis/go-sql-spanner v1.11.1 // @grafana/grafana-search-and-storage
|
||||
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430 // @grafana/alerting-backend
|
||||
github.com/grafana/alerting v0.0.0-20250903141736-c9c007e7f0a8 // @grafana/alerting-backend
|
||||
github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team
|
||||
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
|
||||
@@ -89,7 +89,7 @@ require (
|
||||
github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend
|
||||
github.com/grafana/grafana-app-sdk v0.35.1 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana-app-sdk/logging v0.35.1 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana-aws-sdk v0.31.5 // @grafana/aws-datasources
|
||||
github.com/grafana/grafana-aws-sdk v0.31.6-0.20250918141554-0b96cca5e46b // @grafana/aws-datasources
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // @grafana/partner-datasources
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/grafana-google-sdk-go v0.2.1 // @grafana/partner-datasources
|
||||
@@ -276,25 +276,25 @@ require (
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/at-wat/mqtt-go v0.19.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.0 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
||||
github.com/aws/smithy-go v1.20.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 // indirect
|
||||
github.com/aws/smithy-go v1.23.0 // @grafana/aws-datasources
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
@@ -370,7 +370,7 @@ require (
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-openapi/validate v0.24.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/gogo/googleapis v1.4.1 // indirect
|
||||
@@ -581,6 +581,14 @@ require (
|
||||
|
||||
require github.com/urfave/cli/v3 v3.3.8 // @grafana/grafana-backend-group
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.50.1 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.58.0 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.252.0 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/oam v1.22.3 // @grafana/aws-datasources
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.30.4 // @grafana/aws-datasources
|
||||
)
|
||||
|
||||
require github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
|
||||
// Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream
|
||||
|
||||
78
go.sum
78
go.sum
@@ -843,44 +843,54 @@ github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2z
|
||||
github.com/aws/aws-sdk-go v1.50.29/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
|
||||
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.0/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.8 h1:kQjtOLlTU4m4A64TsRcqwNChhGCwaPBt+zCQt/oWsHU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.8/go.mod h1:QPpc7IgljrKwH0+E6/KolCgr4WPLerURiU592AYzfSY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 h1:zmc9e1q90wMn8wQbjryy8IwA6Q4XlaL9Bx2zIqdNNbk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.12/go.mod h1:3VzdRDR5u3sSJRI4kYcOSIBbeYsgtVk7dG5R/U6qLWY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 h1:Is2tPmieqGS2edBnmOJIbdvOA6Op+rRpaYR60iBAwXM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7/go.mod h1:F1i5V5421EGci570yABvpIXgRIBPb5JM+lSkHF6Dq5w=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 h1:zeN9UtUlA6FTx0vFSayxSX32HDw73Yb6Hh2izDSFxXY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 h1:UCxq0X9O3xrlENdKf1r9eRJoKz/b0AfGkpp3a7FPlhg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7/go.mod h1:rHRoJUNUASj5Z/0eqI4w32vKvC7atoWR0jC+IkmVH8k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 h1:Y6DTZUn7ZUC4th9FMBbo8LVE+1fyq3ofw+tRwkUd3PY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7/go.mod h1:x3XE6vMnU9QvHN/Wrx2s44kwzV2o2g5x/siw4ZUJ9g8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.50.1 h1:OSye2F+X+KfxEdbrOT3x+p7L3kr5zPtm3BMkNWGVXQ8=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.50.1/go.mod h1:bNNaZaAX81KIuYDaj5ODgZwA1ybBJzpDeKYoNxEGGqw=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.58.0 h1:XH0kj0KcoKd+BAadpiS83/Wf+25q4FmH3gDei4u+PzA=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.58.0/go.mod h1:ptJgRWK9opQK1foOTBKUg3PokkKA0/xcTXWIxwliaIY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.252.0 h1:4C/MhyZWR0Bk+7QLVvNxa4xhwEnqm1KclA5MCMepjF0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.252.0/go.mod h1:MXJiLJZtMqb2dVXgEIn35d5+7MqLd4r8noLen881kpk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 h1:mLgc5QIgOy26qyh5bvW+nDoAppxgn3J2WV3m9ewq7+8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7/go.mod h1:wXb/eQnqt8mDQIQTTmcw58B5mYGxzLGZGK8PWNFZ0BA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg=
|
||||
github.com/aws/aws-sdk-go-v2/service/oam v1.22.3 h1:DCKlfaaVB7mE7dtfGD60gWX1Y5nc+yh8qcBHvpdkRqs=
|
||||
github.com/aws/aws-sdk-go-v2/service/oam v1.22.3/go.mod h1:uSLwrlkn0YO7P4xzMy4yJDgyyi6BYzZA73D0iv5gPpo=
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.30.4 h1:LmoqYCi723i8jvkALGA7E+1GeaOc2OHZNLdkwp7cjZA=
|
||||
github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.30.4/go.mod h1:KV1rGdzLiPDfq5EId56EPFzKL5f3FQ8vB4kN/RkkVC4=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
|
||||
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
|
||||
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 h1:7PKX3VYsZ8LUWceVRuv0+PU+E7OtQb1lgmi5vmUE9CM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3/go.mod h1:Ql6jE9kyyWI5JHn+61UT/Y5Z0oyVJGmgmJbZD5g4unY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 h1:e0XBRn3AptQotkyBFrHAxFB8mDhAIOfsG+7KyJ0dg98=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4/go.mod h1:XclEty74bsGBCr1s0VSaA11hQ4ZidK4viWK7rRfO88I=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 h1:PR00NXRYgY4FWHqOGx3fC3lhVKjsp1GdloDv2ynMSd8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4/go.mod h1:Z+Gd23v97pX9zK97+tX4ppAgqCt3Z2dIXB02CtBncK8=
|
||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f/go.mod h1:2stgcRjl6QmW+gU2h5E7BQXg4HU0gzxKWDuT5HviN9s=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1ctzIu4V3bfCNJ39BiOPSm1gHFlFjTkRE0=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
|
||||
@@ -1343,8 +1353,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y=
|
||||
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM=
|
||||
github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I=
|
||||
@@ -1571,8 +1581,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430 h1:qT0D7AIV0GRu8JUrSJYuyzj86kqLgksKQjwD++DqyOM=
|
||||
github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430/go.mod h1:3ER/8BhIEhvrddcztLQSc5ez1f1jNHIPdquc1F+DzOw=
|
||||
github.com/grafana/alerting v0.0.0-20250903141736-c9c007e7f0a8 h1:tXDlMjugyEbsrSJC2Np/9ZvBzEoa1xB+KGHUBnvPIFw=
|
||||
github.com/grafana/alerting v0.0.0-20250903141736-c9c007e7f0a8/go.mod h1:3ER/8BhIEhvrddcztLQSc5ez1f1jNHIPdquc1F+DzOw=
|
||||
github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d h1:TDVZemfYeJHPyXeYCnqL7BQqsa+mpaZYth/Qm3TKaT8=
|
||||
github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM=
|
||||
@@ -1595,8 +1605,8 @@ github.com/grafana/grafana-app-sdk v0.35.1 h1:zEXubzsQrxGBOzXJJMBwhEClC/tvPi0sfK
|
||||
github.com/grafana/grafana-app-sdk v0.35.1/go.mod h1:Zx5MkVppYK+ElSDUAR6+fjzOVo6I/cIgk+ty+LmNOxI=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.35.1 h1:taVpl+RoixTYl0JBJGhH+fPVmwA9wvdwdzJTZsv9buM=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.35.1/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk=
|
||||
github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX/Gh3FZKBE=
|
||||
github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM=
|
||||
github.com/grafana/grafana-aws-sdk v0.31.6-0.20250918141554-0b96cca5e46b h1:25XvbNSo8Sgi3MBFOtToRvlbXwfQ44nTb/NYCpZxUJE=
|
||||
github.com/grafana/grafana-aws-sdk v0.31.6-0.20250918141554-0b96cca5e46b/go.mod h1:2JJbVLc3jjWu0aAQx4bhPa2ChOUjLs9x7ime6mB4O+o=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo=
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 h1:S4kHwr//AqhtL9xHBtz1gqVgZQeCRGTxjgsRBAkpjKY=
|
||||
|
||||
645
go.work.sum
645
go.work.sum
File diff suppressed because it is too large
Load Diff
@@ -10,11 +10,13 @@ export default [
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-data')],
|
||||
treeshake: false,
|
||||
},
|
||||
{
|
||||
input: 'src/unstable.ts',
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-data')],
|
||||
treeshake: false,
|
||||
},
|
||||
tsDeclarationOutput(pkg),
|
||||
tsDeclarationOutput(pkg, {
|
||||
|
||||
@@ -10,6 +10,7 @@ export default [
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-e2e-selectors')],
|
||||
treeshake: false,
|
||||
},
|
||||
tsDeclarationOutput(pkg),
|
||||
];
|
||||
|
||||
@@ -10,6 +10,7 @@ export default [
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-flamegraph')],
|
||||
treeshake: false,
|
||||
},
|
||||
tsDeclarationOutput(pkg),
|
||||
];
|
||||
|
||||
@@ -11,6 +11,7 @@ export default [
|
||||
input: entryPoint,
|
||||
plugins: [...plugins, image()],
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-prometheus')],
|
||||
treeshake: false,
|
||||
},
|
||||
tsDeclarationOutput(pkg),
|
||||
];
|
||||
|
||||
@@ -10,11 +10,13 @@ export default [
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-runtime')],
|
||||
treeshake: false,
|
||||
},
|
||||
{
|
||||
input: 'src/unstable.ts',
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-runtime')],
|
||||
treeshake: false,
|
||||
},
|
||||
tsDeclarationOutput(pkg),
|
||||
tsDeclarationOutput(pkg, {
|
||||
|
||||
@@ -15,6 +15,7 @@ export default [
|
||||
input: entryPoint,
|
||||
plugins,
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-schema')],
|
||||
treeshake: false,
|
||||
},
|
||||
tsDeclarationOutput(pkg, { input: './dist/esm/index.d.ts' }),
|
||||
{
|
||||
@@ -31,5 +32,6 @@ export default [
|
||||
format: 'esm',
|
||||
dir: path.dirname(pkg.publishConfig.module),
|
||||
},
|
||||
treeshake: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -24,6 +24,7 @@ export default [
|
||||
}),
|
||||
],
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-ui')],
|
||||
treeshake: false,
|
||||
},
|
||||
{
|
||||
input: 'src/unstable.ts',
|
||||
@@ -36,6 +37,7 @@ export default [
|
||||
}),
|
||||
],
|
||||
output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-ui')],
|
||||
treeshake: false,
|
||||
},
|
||||
tsDeclarationOutput(pkg),
|
||||
tsDeclarationOutput(pkg, {
|
||||
|
||||
@@ -41,7 +41,7 @@ var getViewIndex = func() string {
|
||||
return viewIndex
|
||||
}
|
||||
|
||||
// Only allow redirects that start with an alphanumerical character, a dash or an underscore.
|
||||
// Only allow redirects that start with a slash followed by an alphanumerical character, a dash or an underscore.
|
||||
var redirectRe = regexp.MustCompile(`^/[a-zA-Z0-9-_].*`)
|
||||
|
||||
var (
|
||||
@@ -73,12 +73,17 @@ func (hs *HTTPServer) ValidateRedirectTo(redirectTo string) error {
|
||||
return errForbiddenRedirectTo
|
||||
}
|
||||
|
||||
if to.Path != "/" && !redirectRe.MatchString(to.Path) {
|
||||
return errForbiddenRedirectTo
|
||||
}
|
||||
|
||||
cleanPath := path.Clean(to.Path)
|
||||
// "." is what path.Clean returns for empty paths
|
||||
if cleanPath == "." {
|
||||
return errForbiddenRedirectTo
|
||||
}
|
||||
if to.Path != "/" && !redirectRe.MatchString(cleanPath) {
|
||||
|
||||
if cleanPath != "/" && !redirectRe.MatchString(cleanPath) {
|
||||
return errForbiddenRedirectTo
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestCallResource(t *testing.T) {
|
||||
cfg.StaticRootPath = staticRootPath
|
||||
cfg.Azure = &azsettings.AzureSettings{}
|
||||
|
||||
coreRegistry := coreplugin.ProvideCoreRegistry(tracing.InitializeTracerForTest(), nil, &cloudwatch.CloudWatchService{}, nil, nil, nil, nil,
|
||||
coreRegistry := coreplugin.ProvideCoreRegistry(tracing.InitializeTracerForTest(), nil, &cloudwatch.Service{}, nil, nil, nil, nil,
|
||||
nil, nil, nil, nil, testdatasource.ProvideService(), nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
testCtx := pluginsintegration.CreateIntegrationTestCtx(t, cfg, coreRegistry)
|
||||
|
||||
@@ -166,6 +166,7 @@ func TestHTTPServer_RotateUserAuthTokenRedirect(t *testing.T) {
|
||||
|
||||
// Invalid redirects should be converted to root
|
||||
{"backslash domain", `/\grafana.com`, "/"},
|
||||
{"backslash domain at the start of the path", `/\grafana.com/../a`, "/"},
|
||||
{"traversal backslash domain", `/a/../\grafana.com`, "/"},
|
||||
{"double slash", "//grafana", "/"},
|
||||
{"missing initial slash", "missingInitialSlash", "/"},
|
||||
@@ -232,7 +233,7 @@ func TestHTTPServer_RotateUserAuthTokenRedirect(t *testing.T) {
|
||||
res, err := server.Send(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 302, redirectStatusCode)
|
||||
assert.Equal(t, redirectCase.expectedUrl, redirectLocation)
|
||||
assert.Equal(t, redirectCase.expectedUrl, redirectLocation, "redirectTo=%s", redirectCase.redirectUrl)
|
||||
|
||||
require.NoError(t, res.Body.Close())
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ runs:
|
||||
GO_MOD_DIR: ${{ inputs.go-mod-dir }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
with:
|
||||
version: 0.18.8
|
||||
verb: run
|
||||
args: go run ./pkg/build/actions/bump-version -version=${VERSION}
|
||||
- name: make gen-cue
|
||||
|
||||
@@ -203,6 +203,10 @@ func doBuild(binaryName, pkg string, opts BuildOpts) error {
|
||||
args = append(args, "-race")
|
||||
}
|
||||
|
||||
// We should not publish Grafana as a Go module, disabling vcs changes the version to (devel)
|
||||
// and works better with SBOM and Vulnerability Scanners.
|
||||
args = append(args, "-buildvcs=false")
|
||||
|
||||
args = append(args, "-o", binary)
|
||||
args = append(args, pkg)
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ func GoLDFlags(flags []LDFlag) string {
|
||||
// GoBuildCommand returns the arguments for go build to be used in 'WithExec'.
|
||||
func GoBuildCommand(output string, ldflags []LDFlag, tags []string, main string) []string {
|
||||
args := []string{"go", "build",
|
||||
// We should not publish Grafana as a Go module, disabling vcs changes the version to (devel)
|
||||
// and works better with SBOM and Vulnerability Scanners.
|
||||
"-buildvcs=false",
|
||||
fmt.Sprintf("-ldflags=\"%s\"", GoLDFlags(ldflags)),
|
||||
fmt.Sprintf("-o=%s", output),
|
||||
"-trimpath",
|
||||
|
||||
102
pkg/cmd/grafana-cli/commands/remove_command_test.go
Normal file
102
pkg/cmd/grafana-cli/commands/remove_command_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
|
||||
)
|
||||
|
||||
func TestRemoveCommand_StaticFS_FailsWithImmutableError(t *testing.T) {
|
||||
t.Run("removeCommand fails with immutable error for plugins using StaticFS", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
pluginID := "test-plugin"
|
||||
pluginDir := filepath.Join(tmpDir, pluginID)
|
||||
|
||||
err := os.MkdirAll(pluginDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
pluginJSON := `{
|
||||
"id": "test-plugin",
|
||||
"name": "Test Plugin",
|
||||
"type": "datasource",
|
||||
"info": {
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}`
|
||||
err = os.WriteFile(filepath.Join(pluginDir, "plugin.json"), []byte(pluginJSON), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmdLine := createCliContextWithArgs(t, []string{pluginID}, "pluginsDir", tmpDir)
|
||||
require.NotNil(t, cmdLine)
|
||||
|
||||
// Verify plugin directory exists before attempting removal
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.NoError(t, err, "Plugin directory should exist before removal attempt")
|
||||
|
||||
err = removeCommand(cmdLine)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify plugin directory has been removed
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.ErrorIs(t, err, os.ErrNotExist)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoveCommand_PluginNotFound(t *testing.T) {
|
||||
t.Run("removeCommand should handle missing plugin gracefully", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cmdLine := createCliContextWithArgs(t, []string{"non-existent-plugin"}, "pluginsDir", tmpDir)
|
||||
require.NotNil(t, cmdLine)
|
||||
|
||||
err := removeCommand(cmdLine)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoveCommand_MissingPluginParameter(t *testing.T) {
|
||||
t.Run("removeCommand should error when no plugin ID is provided", func(t *testing.T) {
|
||||
cmdLine := createCliContextWithArgs(t, []string{})
|
||||
require.NotNil(t, cmdLine)
|
||||
|
||||
err := removeCommand(cmdLine)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "missing plugin parameter")
|
||||
})
|
||||
}
|
||||
|
||||
// createCliContextWithArgs creates a CLI context with the specified arguments and optional flag key-value pairs.
|
||||
// Usage: createCliContextWithArgs(t, []string{"plugin-id"}, "pluginsDir", "/path/to/plugins", "flag2", "value2")
|
||||
func createCliContextWithArgs(t *testing.T, args []string, flagPairs ...string) *utils.ContextCommandLine {
|
||||
if len(flagPairs)%2 != 0 {
|
||||
t.Fatalf("flagPairs must be provided in key-value pairs, got %d arguments", len(flagPairs))
|
||||
}
|
||||
|
||||
app := &cli.App{
|
||||
Name: "grafana",
|
||||
}
|
||||
|
||||
flagSet := flag.NewFlagSet("test", 0)
|
||||
|
||||
// Add flags from the key-value pairs
|
||||
for i := 0; i < len(flagPairs); i += 2 {
|
||||
key := flagPairs[i]
|
||||
value := flagPairs[i+1]
|
||||
flagSet.String(key, "", "")
|
||||
err := flagSet.Set(key, value)
|
||||
require.NoError(t, err, "Failed to set flag %s=%s", key, value)
|
||||
}
|
||||
|
||||
err := flagSet.Parse(args)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := cli.NewContext(app, flagSet, nil)
|
||||
return &utils.ContextCommandLine{
|
||||
Context: ctx,
|
||||
}
|
||||
}
|
||||
150
pkg/cmd/grafana-cli/commands/upgrade_command_test.go
Normal file
150
pkg/cmd/grafana-cli/commands/upgrade_command_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
||||
)
|
||||
|
||||
func TestUpgradeCommand(t *testing.T) {
|
||||
t.Run("Plugin is removed even if upgrade fails", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
pluginID := "test-upgrade-plugin"
|
||||
pluginDir := filepath.Join(tmpDir, pluginID)
|
||||
|
||||
err := os.MkdirAll(pluginDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
pluginJSON := `{
|
||||
"id": "test-upgrade-plugin",
|
||||
"name": "Test Upgrade Plugin",
|
||||
"type": "datasource",
|
||||
"info": {
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}`
|
||||
err = os.WriteFile(filepath.Join(pluginDir, "plugin.json"), []byte(pluginJSON), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a mock HTTP server that returns plugin info with a newer version
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Handle plugin info request
|
||||
if r.URL.Path == "/repo/"+pluginID {
|
||||
plugin := models.Plugin{
|
||||
ID: pluginID,
|
||||
Versions: []models.Version{
|
||||
{
|
||||
Version: "2.0.0", // Newer than the local version (1.0.0)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(plugin)
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
// For any other request (like installation), return 500 to cause the upgrade to fail
|
||||
// after the removal attempt, which is what we want to test
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, err = w.Write([]byte("Server error"))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
// Use our test implementation that properly implements GcomToken()
|
||||
cmdLine := newTestCommandLine([]string{pluginID}, tmpDir, mockServer.URL)
|
||||
|
||||
// Verify plugin directory exists before attempting upgrade
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = upgradeCommand(cmdLine)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "API returned invalid status: 500 Internal Server Error")
|
||||
|
||||
// Verify plugin directory was removed during the removal step
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpgradeCommand_PluginNotFound(t *testing.T) {
|
||||
t.Run("upgradeCommand should handle missing plugin gracefully", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cmdLine := createCliContextWithArgs(t, []string{"non-existent-plugin"}, "pluginsDir", tmpDir)
|
||||
require.NotNil(t, cmdLine)
|
||||
|
||||
err := upgradeCommand(cmdLine)
|
||||
require.Error(t, err)
|
||||
// Should fail trying to find the local plugin
|
||||
require.Contains(t, err.Error(), "could not find plugin non-existent-plugin")
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpgradeCommand_MissingPluginParameter(t *testing.T) {
|
||||
t.Run("upgradeCommand should error when no plugin ID is provided", func(t *testing.T) {
|
||||
cmdLine := createCliContextWithArgs(t, []string{})
|
||||
require.NotNil(t, cmdLine)
|
||||
|
||||
err := upgradeCommand(cmdLine)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "please specify plugin to update")
|
||||
})
|
||||
}
|
||||
|
||||
// Simple args implementation
|
||||
type simpleArgs []string
|
||||
|
||||
func (a simpleArgs) First() string {
|
||||
if len(a) > 0 {
|
||||
return a[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (a simpleArgs) Get(int) string { return "" }
|
||||
func (a simpleArgs) Tail() []string { return nil }
|
||||
func (a simpleArgs) Len() int { return len(a) }
|
||||
func (a simpleArgs) Present() bool { return len(a) > 0 }
|
||||
func (a simpleArgs) Slice() []string { return []string(a) }
|
||||
|
||||
// Base struct with default implementations for unused CommandLine methods
|
||||
type baseCommandLine struct{}
|
||||
|
||||
func (b baseCommandLine) ShowHelp() error { return nil }
|
||||
func (b baseCommandLine) ShowVersion() {}
|
||||
func (b baseCommandLine) Application() *cli.App { return nil }
|
||||
func (b baseCommandLine) Int(_ string) int { return 0 }
|
||||
func (b baseCommandLine) String(_ string) string { return "" }
|
||||
func (b baseCommandLine) StringSlice(_ string) []string { return nil }
|
||||
func (b baseCommandLine) FlagNames() []string { return nil }
|
||||
func (b baseCommandLine) Generic(_ string) any { return nil }
|
||||
func (b baseCommandLine) Bool(_ string) bool { return false }
|
||||
func (b baseCommandLine) PluginURL() string { return "" }
|
||||
func (b baseCommandLine) GcomToken() string { return "" }
|
||||
|
||||
// Test implementation - only implements what we actually need
|
||||
type testCommandLine struct {
|
||||
baseCommandLine // Embedded struct provides default implementations
|
||||
args simpleArgs
|
||||
pluginDir string
|
||||
repoURL string
|
||||
}
|
||||
|
||||
func newTestCommandLine(args []string, pluginDir, repoURL string) *testCommandLine {
|
||||
return &testCommandLine{args: simpleArgs(args), pluginDir: pluginDir, repoURL: repoURL}
|
||||
}
|
||||
|
||||
// Only implement the methods actually used by upgradeCommand
|
||||
func (t *testCommandLine) Args() cli.Args { return t.args }
|
||||
func (t *testCommandLine) PluginDirectory() string { return t.pluginDir }
|
||||
func (t *testCommandLine) PluginRepoURL() string { return t.repoURL }
|
||||
@@ -4,9 +4,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
awssdk "github.com/grafana/grafana-aws-sdk/pkg/sigv4"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsauth"
|
||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/mwitkow/go-conntrack"
|
||||
|
||||
@@ -46,21 +44,7 @@ func New(cfg *setting.Cfg, validator validations.DataSourceRequestURLValidator,
|
||||
|
||||
// SigV4 signing should be performed after all headers are added
|
||||
if cfg.SigV4AuthEnabled {
|
||||
authSettings := awsds.AuthSettings{
|
||||
AllowedAuthProviders: cfg.AWSAllowedAuthProviders,
|
||||
AssumeRoleEnabled: cfg.AWSAssumeRoleEnabled,
|
||||
ExternalID: cfg.AWSExternalId,
|
||||
ListMetricsPageLimit: cfg.AWSListMetricsPageLimit,
|
||||
SecureSocksDSProxyEnabled: cfg.SecureSocksDSProxy.Enabled,
|
||||
}
|
||||
if cfg.AWSSessionDuration != "" {
|
||||
sessionDuration, err := gtime.ParseDuration(cfg.AWSSessionDuration)
|
||||
if err == nil {
|
||||
authSettings.SessionDuration = &sessionDuration
|
||||
}
|
||||
}
|
||||
|
||||
middlewares = append(middlewares, awssdk.SigV4MiddlewareWithAuthSettings(cfg.SigV4VerboseLogging, authSettings))
|
||||
middlewares = append(middlewares, awsauth.NewSigV4Middleware())
|
||||
}
|
||||
|
||||
setDefaultTimeoutOptions(cfg)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/validations"
|
||||
|
||||
awssdk "github.com/grafana/grafana-aws-sdk/pkg/sigv4"
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsauth"
|
||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -63,7 +63,7 @@ func TestHTTPClientProvider(t *testing.T) {
|
||||
require.Equal(t, sdkhttpclient.ResponseLimitMiddlewareName, o.Middlewares[6].(sdkhttpclient.MiddlewareName).MiddlewareName())
|
||||
require.Equal(t, HostRedirectValidationMiddlewareName, o.Middlewares[7].(sdkhttpclient.MiddlewareName).MiddlewareName())
|
||||
require.Equal(t, sdkhttpclient.ErrorSourceMiddlewareName, o.Middlewares[8].(sdkhttpclient.MiddlewareName).MiddlewareName())
|
||||
require.Equal(t, awssdk.SigV4MiddlewareName, o.Middlewares[9].(sdkhttpclient.MiddlewareName).MiddlewareName())
|
||||
require.Equal(t, awsauth.NewSigV4Middleware().(sdkhttpclient.MiddlewareName).MiddlewareName(), o.Middlewares[9].(sdkhttpclient.MiddlewareName).MiddlewareName())
|
||||
})
|
||||
|
||||
t.Run("When creating new provider and http logging is enabled for one plugin, it should apply expected middleware", func(t *testing.T) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
// Only allow redirects that start with an alphanumerical character, a dash or an underscore.
|
||||
// Only allow redirects that start with a slash followed by an alphanumerical character, a dash or an underscore.
|
||||
var redirectRe = regexp.MustCompile(`^/?[a-zA-Z0-9-_].*`)
|
||||
|
||||
// OrgRedirect changes org and redirects users if the
|
||||
@@ -66,6 +66,9 @@ func OrgRedirect(cfg *setting.Cfg, userSvc user.Service) web.Handler {
|
||||
}
|
||||
|
||||
func validRedirectPath(p string) bool {
|
||||
if p != "" && p != "/" && !redirectRe.MatchString(p) {
|
||||
return false
|
||||
}
|
||||
cleanPath := path.Clean(p)
|
||||
return cleanPath == "." || cleanPath == "/" || redirectRe.MatchString(cleanPath)
|
||||
}
|
||||
|
||||
@@ -72,13 +72,18 @@ func TestOrgRedirectMiddleware(t *testing.T) {
|
||||
})
|
||||
|
||||
middlewareScenario(t, "when redirecting to an invalid path", func(t *testing.T, sc *scenarioContext) {
|
||||
sc.withIdentity(&authn.Identity{})
|
||||
testPaths := []string{
|
||||
url.QueryEscape(`/\example.com`),
|
||||
`/%2fexample.com`,
|
||||
}
|
||||
for _, path := range testPaths {
|
||||
sc.withIdentity(&authn.Identity{})
|
||||
|
||||
path := url.QueryEscape(`/\example.com`)
|
||||
sc.m.Get(url.QueryEscape(path), sc.defaultHandler)
|
||||
sc.fakeReq("GET", fmt.Sprintf("%s?orgId=3", path)).exec()
|
||||
sc.m.Get(url.QueryEscape(path), sc.defaultHandler)
|
||||
sc.fakeReq("GET", fmt.Sprintf("%s?orgId=3", path)).exec()
|
||||
|
||||
require.Equal(t, 404, sc.resp.Code)
|
||||
require.Equal(t, 404, sc.resp.Code, "path: %s", path)
|
||||
}
|
||||
})
|
||||
|
||||
middlewareScenario(t, "works correctly when grafana is served under a subpath", func(t *testing.T, sc *scenarioContext) {
|
||||
|
||||
@@ -94,7 +94,7 @@ func NewRegistry(store map[string]backendplugin.PluginFactoryFunc) *Registry {
|
||||
}
|
||||
}
|
||||
|
||||
func ProvideCoreRegistry(tracer tracing.Tracer, am *azuremonitor.Service, cw *cloudwatch.CloudWatchService, cm *cloudmonitoring.Service,
|
||||
func ProvideCoreRegistry(tracer tracing.Tracer, am *azuremonitor.Service, cw *cloudwatch.Service, cm *cloudmonitoring.Service,
|
||||
es *elasticsearch.Service, grap *graphite.Service, idb *influxdb.Service, lk *loki.Service, otsdb *opentsdb.Service,
|
||||
pr *prometheus.Service, t *tempo.Service, td *testdatasource.Service, pg *postgres.Service, my *mysql.Service,
|
||||
ms *mssql.Service, graf *grafanads.Service, pyroscope *pyroscope.Service, parca *parca.Service, zipkin *zipkin.Service, jaeger *jaeger.Service) *Registry {
|
||||
@@ -102,7 +102,7 @@ func ProvideCoreRegistry(tracer tracing.Tracer, am *azuremonitor.Service, cw *cl
|
||||
sdktracing.InitDefaultTracer(tracer)
|
||||
|
||||
return NewRegistry(map[string]backendplugin.PluginFactoryFunc{
|
||||
CloudWatch: asBackendPlugin(cw.Executor),
|
||||
CloudWatch: asBackendPlugin(cw),
|
||||
CloudMonitoring: asBackendPlugin(cm),
|
||||
AzureMonitor: asBackendPlugin(am),
|
||||
Elasticsearch: asBackendPlugin(es),
|
||||
@@ -217,7 +217,7 @@ func NewPlugin(pluginID string, cfg *setting.Cfg, httpClientProvider *httpclient
|
||||
jsonData.AliasIDs = append(jsonData.AliasIDs, TestDataAlias)
|
||||
svc = testdatasource.ProvideService()
|
||||
case CloudWatch:
|
||||
svc = cloudwatch.ProvideService(httpClientProvider).Executor
|
||||
svc = cloudwatch.ProvideService()
|
||||
case CloudMonitoring:
|
||||
svc = cloudmonitoring.ProvideService(httpClientProvider)
|
||||
case AzureMonitor:
|
||||
|
||||
@@ -236,6 +236,15 @@ func (f StaticFS) Files() ([]string, error) {
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (f StaticFS) Remove() error {
|
||||
if remover, ok := f.FS.(FSRemover); ok {
|
||||
if err := remover.Remove(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalFile implements a fs.File for accessing the local filesystem.
|
||||
type LocalFile struct {
|
||||
f *os.File
|
||||
|
||||
@@ -270,12 +270,27 @@ func TestStaticFS(t *testing.T) {
|
||||
require.Equal(t, []string{allowedFn, deniedFn}, files)
|
||||
})
|
||||
|
||||
t.Run("staticfs filters underelying fs's files", func(t *testing.T) {
|
||||
t.Run("staticfs filters underlying fs's files", func(t *testing.T) {
|
||||
files, err := staticFS.Files()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{allowedFn}, files)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("FSRemover interface implementation verification", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
lfs := NewLocalFS(tmpDir)
|
||||
var localFSInterface FS = lfs
|
||||
_, isRemover := localFSInterface.(FSRemover)
|
||||
require.True(t, isRemover)
|
||||
|
||||
sfs, err := NewStaticFS(localFS)
|
||||
require.NoError(t, err)
|
||||
var staticFSInterface FS = sfs
|
||||
_, isRemover = staticFSInterface.(FSRemover)
|
||||
require.True(t, isRemover)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFSTwoDotsInFileName ensures that LocalFS and StaticFS allow two dots in file names.
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
@@ -416,3 +418,100 @@ func createPlugin(t *testing.T, pluginID string, class plugins.Class, managed, b
|
||||
func testCompatOpts() plugins.AddOpts {
|
||||
return plugins.NewAddOpts("10.0.0", runtime.GOOS, runtime.GOARCH, "")
|
||||
}
|
||||
|
||||
func TestPluginInstaller_Removal(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
t.Run("LocalFS plugin removal succeeds via installer.Remove", func(t *testing.T) {
|
||||
pluginDir := filepath.Join(tmpDir, "localfs-plugin")
|
||||
err := os.MkdirAll(pluginDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
pluginJSON := `{
|
||||
"id": "localfs-plugin",
|
||||
"name": "LocalFS Plugin",
|
||||
"type": "datasource",
|
||||
"info": {
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}`
|
||||
err = os.WriteFile(filepath.Join(pluginDir, "plugin.json"), []byte(pluginJSON), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
localFS := plugins.NewLocalFS(pluginDir)
|
||||
pluginV1 := createPlugin(t, "localfs-plugin", plugins.ClassExternal, true, true, func(plugin *plugins.Plugin) {
|
||||
plugin.Info.Version = "1.0.0"
|
||||
plugin.FS = localFS
|
||||
})
|
||||
|
||||
registry := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
"localfs-plugin": pluginV1,
|
||||
},
|
||||
}
|
||||
|
||||
loader := &fakes.FakeLoader{
|
||||
UnloadFunc: func(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
|
||||
return p, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
inst := New(registry, loader, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{})
|
||||
err = inst.Remove(context.Background(), "localfs-plugin", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
})
|
||||
|
||||
t.Run("StaticFS plugin removal is skipped via installer.Remove", func(t *testing.T) {
|
||||
pluginDir := filepath.Join(tmpDir, "staticfs-plugin")
|
||||
err := os.MkdirAll(pluginDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
pluginJSON := `{
|
||||
"id": "staticfs-plugin",
|
||||
"name": "StaticFS Plugin",
|
||||
"type": "datasource",
|
||||
"info": {
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}`
|
||||
err = os.WriteFile(filepath.Join(pluginDir, "plugin.json"), []byte(pluginJSON), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
localFS := plugins.NewLocalFS(pluginDir)
|
||||
staticFS, err := plugins.NewStaticFS(localFS)
|
||||
require.NoError(t, err)
|
||||
|
||||
pluginV1 := createPlugin(t, "staticfs-plugin", plugins.ClassExternal, true, true, func(plugin *plugins.Plugin) {
|
||||
plugin.Info.Version = "1.0.0"
|
||||
plugin.FS = staticFS
|
||||
})
|
||||
|
||||
registry := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
"staticfs-plugin": pluginV1,
|
||||
},
|
||||
}
|
||||
|
||||
loader := &fakes.FakeLoader{
|
||||
UnloadFunc: func(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
|
||||
return p, nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
inst := New(registry, loader, &fakes.FakePluginRepo{}, &fakes.FakePluginStorage{}, storage.SimpleDirNameGeneratorFunc, &fakes.FakeAuthService{})
|
||||
err = inst.Remove(context.Background(), "staticfs-plugin", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(pluginDir)
|
||||
require.ErrorIs(t, err, os.ErrNotExist)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package datasource
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsauth"
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/sigv4"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
)
|
||||
@@ -16,8 +16,7 @@ func contextualMiddlewares(ctx context.Context) context.Context {
|
||||
|
||||
sigv4Settings := awsds.ReadSigV4Settings(ctx)
|
||||
if sigv4Settings.Enabled {
|
||||
authSettings, _ := awsds.ReadAuthSettingsFromContext(ctx)
|
||||
ctx = httpclient.WithContextualMiddleware(ctx, sigv4.SigV4MiddlewareWithAuthSettings(sigv4Settings.VerboseLogging, *authSettings))
|
||||
ctx = httpclient.WithContextualMiddleware(ctx, awsauth.NewSigV4Middleware())
|
||||
}
|
||||
|
||||
return ctx
|
||||
|
||||
@@ -351,7 +351,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser
|
||||
ossDataSourceRequestURLValidator := validations.ProvideURLValidator()
|
||||
httpclientProvider := httpclientprovider.New(cfg, ossDataSourceRequestURLValidator, tracingService)
|
||||
azuremonitorService := azuremonitor.ProvideService(httpclientProvider)
|
||||
cloudWatchService := cloudwatch.ProvideService(httpclientProvider)
|
||||
cloudwatchService := cloudwatch.ProvideService()
|
||||
cloudmonitoringService := cloudmonitoring.ProvideService(httpclientProvider)
|
||||
elasticsearchService := elasticsearch.ProvideService(httpclientProvider)
|
||||
graphiteService := graphite.ProvideService(httpclientProvider, tracingService)
|
||||
@@ -446,7 +446,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser
|
||||
parcaService := parca.ProvideService(httpclientProvider)
|
||||
zipkinService := zipkin.ProvideService(httpclientProvider)
|
||||
jaegerService := jaeger.ProvideService(httpclientProvider)
|
||||
corepluginRegistry := coreplugin.ProvideCoreRegistry(tracingService, azuremonitorService, cloudWatchService, cloudmonitoringService, elasticsearchService, graphiteService, influxdbService, lokiService, opentsdbService, prometheusService, tempoService, testdatasourceService, postgresService, mysqlService, mssqlService, grafanadsService, pyroscopeService, parcaService, zipkinService, jaegerService)
|
||||
corepluginRegistry := coreplugin.ProvideCoreRegistry(tracingService, azuremonitorService, cloudwatchService, cloudmonitoringService, elasticsearchService, graphiteService, influxdbService, lokiService, opentsdbService, prometheusService, tempoService, testdatasourceService, postgresService, mysqlService, mssqlService, grafanadsService, pyroscopeService, parcaService, zipkinService, jaegerService)
|
||||
providerService := provider2.ProvideService(corepluginRegistry)
|
||||
processService := process.ProvideService()
|
||||
retrieverService := retriever.ProvideService(sqlStore, apikeyService, kvStore, userService, orgService)
|
||||
@@ -861,7 +861,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface {
|
||||
ossDataSourceRequestURLValidator := validations.ProvideURLValidator()
|
||||
httpclientProvider := httpclientprovider.New(cfg, ossDataSourceRequestURLValidator, tracingService)
|
||||
azuremonitorService := azuremonitor.ProvideService(httpclientProvider)
|
||||
cloudWatchService := cloudwatch.ProvideService(httpclientProvider)
|
||||
cloudwatchService := cloudwatch.ProvideService()
|
||||
cloudmonitoringService := cloudmonitoring.ProvideService(httpclientProvider)
|
||||
elasticsearchService := elasticsearch.ProvideService(httpclientProvider)
|
||||
graphiteService := graphite.ProvideService(httpclientProvider, tracingService)
|
||||
@@ -956,7 +956,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface {
|
||||
parcaService := parca.ProvideService(httpclientProvider)
|
||||
zipkinService := zipkin.ProvideService(httpclientProvider)
|
||||
jaegerService := jaeger.ProvideService(httpclientProvider)
|
||||
corepluginRegistry := coreplugin.ProvideCoreRegistry(tracingService, azuremonitorService, cloudWatchService, cloudmonitoringService, elasticsearchService, graphiteService, influxdbService, lokiService, opentsdbService, prometheusService, tempoService, testdatasourceService, postgresService, mysqlService, mssqlService, grafanadsService, pyroscopeService, parcaService, zipkinService, jaegerService)
|
||||
corepluginRegistry := coreplugin.ProvideCoreRegistry(tracingService, azuremonitorService, cloudwatchService, cloudmonitoringService, elasticsearchService, graphiteService, influxdbService, lokiService, opentsdbService, prometheusService, tempoService, testdatasourceService, postgresService, mysqlService, mssqlService, grafanadsService, pyroscopeService, parcaService, zipkinService, jaegerService)
|
||||
providerService := provider2.ProvideService(corepluginRegistry)
|
||||
processService := process.ProvideService()
|
||||
retrieverService := retriever.ProvideService(sqlStore, apikeyService, kvStore, userService, orgService)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -28,16 +29,21 @@ var (
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
db db.DB
|
||||
features featuremgmt.FeatureToggles
|
||||
dashSvc dashboards.DashboardService
|
||||
db db.DB
|
||||
features featuremgmt.FeatureToggles
|
||||
dashSvc dashboards.DashboardService
|
||||
searchDashboardsPageLimit int64
|
||||
}
|
||||
|
||||
func NewAuthService(db db.DB, features featuremgmt.FeatureToggles, dashSvc dashboards.DashboardService) *AuthService {
|
||||
func NewAuthService(db db.DB, features featuremgmt.FeatureToggles, dashSvc dashboards.DashboardService, cfg *setting.Cfg) *AuthService {
|
||||
section := cfg.Raw.Section("annotations")
|
||||
searchDashboardsPageLimit := section.Key("search_dashboards_page_limit").MustInt64(1000)
|
||||
|
||||
return &AuthService{
|
||||
db: db,
|
||||
features: features,
|
||||
dashSvc: dashSvc,
|
||||
db: db,
|
||||
features: features,
|
||||
dashSvc: dashSvc,
|
||||
searchDashboardsPageLimit: searchDashboardsPageLimit,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +148,7 @@ func (authz *AuthService) dashboardsWithVisibleAnnotations(ctx context.Context,
|
||||
SignedInUser: query.SignedInUser,
|
||||
Page: query.Page,
|
||||
Type: filterType,
|
||||
Limit: 1000,
|
||||
Limit: authz.searchDashboardsPageLimit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -207,7 +207,7 @@ func TestIntegrationAuthorize(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
u.Permissions = map[int64]map[string][]string{1: tc.permissions}
|
||||
testutil.SetupRBACPermission(t, sql, role, u)
|
||||
authz := NewAuthService(sql, featuremgmt.WithFeatures(tc.featureToggle), dashSvc)
|
||||
authz := NewAuthService(sql, featuremgmt.WithFeatures(tc.featureToggle), dashSvc, cfg)
|
||||
|
||||
query := annotations.ItemQuery{SignedInUser: u, OrgID: 1}
|
||||
resources, err := authz.Authorize(context.Background(), query)
|
||||
|
||||
@@ -53,7 +53,7 @@ func ProvideService(
|
||||
return &RepositoryImpl{
|
||||
db: db,
|
||||
features: features,
|
||||
authZ: accesscontrol.NewAuthService(db, features, dashSvc),
|
||||
authZ: accesscontrol.NewAuthService(db, features, dashSvc, cfg),
|
||||
reader: read,
|
||||
writer: write,
|
||||
}
|
||||
|
||||
@@ -794,8 +794,9 @@ func (alertRule *AlertRule) Copy() *AlertRule {
|
||||
|
||||
if alertRule.Record != nil {
|
||||
result.Record = &Record{
|
||||
From: alertRule.Record.From,
|
||||
Metric: alertRule.Record.Metric,
|
||||
From: alertRule.Record.From,
|
||||
Metric: alertRule.Record.Metric,
|
||||
TargetDatasourceUID: alertRule.Record.TargetDatasourceUID,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1012,6 +1012,13 @@ func TestAlertRuleCopy(t *testing.T) {
|
||||
copied := rule.Copy()
|
||||
require.NotSame(t, rule.Metadata.PrometheusStyleRule, copied.Metadata.PrometheusStyleRule)
|
||||
})
|
||||
t.Run("should return an exact copy of recording rule", func(t *testing.T) {
|
||||
for i := 0; i < 100; i++ {
|
||||
rule := RuleGen.With(RuleGen.WithAllRecordingRules()).GenerateRef()
|
||||
copied := rule.Copy()
|
||||
require.Empty(t, rule.Diff(copied))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// This test makes sure the default generator
|
||||
@@ -1051,6 +1058,48 @@ func TestGeneratorFillsAllFields(t *testing.T) {
|
||||
require.FailNow(t, "AlertRule generator does not populate fields", "skipped fields: %v", maps.Keys(fields))
|
||||
}
|
||||
|
||||
func TestGeneratorFillsAllRecordingRuleFields(t *testing.T) {
|
||||
ignoredFields := map[string]struct{}{
|
||||
"ID": {},
|
||||
"IsPaused": {},
|
||||
"NoDataState": {},
|
||||
"ExecErrState": {},
|
||||
"Condition": {},
|
||||
"KeepFiringFor": {},
|
||||
"MissingSeriesEvalsToResolve": {},
|
||||
"For": {},
|
||||
"NotificationSettings": {},
|
||||
}
|
||||
|
||||
tpe := reflect.TypeOf(AlertRule{})
|
||||
fields := make(map[string]struct{}, tpe.NumField())
|
||||
for i := 0; i < tpe.NumField(); i++ {
|
||||
if _, ok := ignoredFields[tpe.Field(i).Name]; ok {
|
||||
continue
|
||||
}
|
||||
fields[tpe.Field(i).Name] = struct{}{}
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
rule := RuleGen.With(RuleGen.WithAllRecordingRules()).Generate()
|
||||
v := reflect.ValueOf(rule)
|
||||
|
||||
for j := 0; j < tpe.NumField(); j++ {
|
||||
field := tpe.Field(j)
|
||||
value := v.Field(j)
|
||||
if !value.IsValid() || value.Kind() == reflect.Ptr && value.IsNil() || value.IsZero() {
|
||||
continue
|
||||
}
|
||||
delete(fields, field.Name)
|
||||
if len(fields) == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require.FailNow(t, "AlertRule generator does not populate fields", "skipped fields: %v", maps.Keys(fields))
|
||||
}
|
||||
|
||||
func TestValidateAlertRule(t *testing.T) {
|
||||
t.Run("keepFiringFor", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
|
||||
@@ -561,6 +561,14 @@ func (a *AlertRuleMutators) WithAllRecordingRules() AlertRuleMutator {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AlertRuleMutators) WithoutTargetDataSource() AlertRuleMutator {
|
||||
return func(rule *AlertRule) {
|
||||
if rule.Record != nil {
|
||||
rule.Record.TargetDatasourceUID = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AlertRuleMutators) WithMetric(metric string) AlertRuleMutator {
|
||||
return func(rule *AlertRule) {
|
||||
if rule.Record == nil {
|
||||
@@ -1363,10 +1371,14 @@ func ConvertToRecordingRule(rule *AlertRule) {
|
||||
if rule.Record.Metric == "" {
|
||||
rule.Record.Metric = fmt.Sprintf("some_metric_%s", util.GenerateShortUID())
|
||||
}
|
||||
if rule.Record.TargetDatasourceUID == "" {
|
||||
rule.Record.TargetDatasourceUID = util.GenerateShortUID()
|
||||
}
|
||||
rule.Condition = ""
|
||||
rule.NoDataState = ""
|
||||
rule.ExecErrState = ""
|
||||
rule.For = 0
|
||||
rule.KeepFiringFor = 0
|
||||
rule.NotificationSettings = nil
|
||||
rule.MissingSeriesEvalsToResolve = nil
|
||||
}
|
||||
|
||||
@@ -229,7 +229,11 @@ func (f *RuleStore) ListAlertRules(_ context.Context, q *models.ListAlertRulesQu
|
||||
}
|
||||
}
|
||||
|
||||
ruleList = append(ruleList, r)
|
||||
if q.ReceiverName != "" && (len(r.NotificationSettings) < 1 || r.NotificationSettings[0].Receiver != q.ReceiverName) {
|
||||
continue
|
||||
}
|
||||
copyR := models.CopyRule(r)
|
||||
ruleList = append(ruleList, copyR)
|
||||
}
|
||||
|
||||
return ruleList, nil
|
||||
|
||||
@@ -75,7 +75,7 @@ func TestIntegrationPluginManager(t *testing.T) {
|
||||
|
||||
hcp := httpclient.NewProvider()
|
||||
am := azuremonitor.ProvideService(hcp)
|
||||
cw := cloudwatch.ProvideService(hcp)
|
||||
cw := cloudwatch.ProvideService()
|
||||
cm := cloudmonitoring.ProvideService(hcp)
|
||||
es := elasticsearch.ProvideService(hcp)
|
||||
grap := graphite.ProvideService(hcp, tracer)
|
||||
|
||||
@@ -210,4 +210,12 @@ func AddMigration(mg *migrator.Migrator) {
|
||||
Type: migrator.UniqueIndex,
|
||||
Cols: []string{"org_id", "user_id", "role_id"},
|
||||
}))
|
||||
|
||||
mg.AddMigration("add permission role_id action index", migrator.NewAddIndexMigration(permissionV1, &migrator.Index{
|
||||
Cols: []string{"role_id", "action"},
|
||||
}))
|
||||
|
||||
mg.AddMigration("Remove permission role_id index", migrator.NewDropIndexMigration(permissionV1, &migrator.Index{
|
||||
Cols: []string{"role_id"},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -149,7 +149,12 @@ func (b *Builder) applyFilters() (ordering string) {
|
||||
}
|
||||
}
|
||||
|
||||
b.sql.WriteString("SELECT dashboard.id FROM dashboard")
|
||||
forceIndex := ""
|
||||
if b.Dialect.DriverName() == migrator.MySQL {
|
||||
forceIndex = " FORCE INDEX (IDX_dashboard_title) "
|
||||
}
|
||||
|
||||
b.sql.WriteString(fmt.Sprintf("SELECT dashboard.id FROM dashboard %s", forceIndex))
|
||||
b.sql.WriteString(strings.Join(joins, ""))
|
||||
|
||||
if len(wheres) > 0 {
|
||||
|
||||
@@ -76,25 +76,25 @@ require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/at-wat/mqtt-go v0.19.4 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
||||
github.com/aws/smithy-go v1.20.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 // indirect
|
||||
github.com/aws/smithy-go v1.23.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
@@ -178,7 +178,7 @@ require (
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.2 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
@@ -207,13 +207,13 @@ require (
|
||||
github.com/googleapis/go-sql-spanner v1.11.1 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20250903141736-c9c007e7f0a8 // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect
|
||||
github.com/grafana/grafana-app-sdk v0.35.1 // indirect
|
||||
github.com/grafana/grafana-app-sdk/logging v0.35.1 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v0.31.5 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v0.31.6-0.20250918141554-0b96cca5e46b // indirect
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.277.0 // indirect
|
||||
github.com/grafana/grafana/apps/folder v0.0.0-20250414115220-48647355c37b // indirect
|
||||
|
||||
@@ -740,44 +740,44 @@ github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
|
||||
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
|
||||
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.0/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.8 h1:kQjtOLlTU4m4A64TsRcqwNChhGCwaPBt+zCQt/oWsHU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.8/go.mod h1:QPpc7IgljrKwH0+E6/KolCgr4WPLerURiU592AYzfSY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 h1:zmc9e1q90wMn8wQbjryy8IwA6Q4XlaL9Bx2zIqdNNbk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.12/go.mod h1:3VzdRDR5u3sSJRI4kYcOSIBbeYsgtVk7dG5R/U6qLWY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 h1:Is2tPmieqGS2edBnmOJIbdvOA6Op+rRpaYR60iBAwXM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7/go.mod h1:F1i5V5421EGci570yABvpIXgRIBPb5JM+lSkHF6Dq5w=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 h1:zeN9UtUlA6FTx0vFSayxSX32HDw73Yb6Hh2izDSFxXY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 h1:UCxq0X9O3xrlENdKf1r9eRJoKz/b0AfGkpp3a7FPlhg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7/go.mod h1:rHRoJUNUASj5Z/0eqI4w32vKvC7atoWR0jC+IkmVH8k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 h1:Y6DTZUn7ZUC4th9FMBbo8LVE+1fyq3ofw+tRwkUd3PY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7/go.mod h1:x3XE6vMnU9QvHN/Wrx2s44kwzV2o2g5x/siw4ZUJ9g8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 h1:mLgc5QIgOy26qyh5bvW+nDoAppxgn3J2WV3m9ewq7+8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7/go.mod h1:wXb/eQnqt8mDQIQTTmcw58B5mYGxzLGZGK8PWNFZ0BA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
|
||||
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
|
||||
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 h1:7PKX3VYsZ8LUWceVRuv0+PU+E7OtQb1lgmi5vmUE9CM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3/go.mod h1:Ql6jE9kyyWI5JHn+61UT/Y5Z0oyVJGmgmJbZD5g4unY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 h1:e0XBRn3AptQotkyBFrHAxFB8mDhAIOfsG+7KyJ0dg98=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4/go.mod h1:XclEty74bsGBCr1s0VSaA11hQ4ZidK4viWK7rRfO88I=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 h1:PR00NXRYgY4FWHqOGx3fC3lhVKjsp1GdloDv2ynMSd8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4/go.mod h1:Z+Gd23v97pX9zK97+tX4ppAgqCt3Z2dIXB02CtBncK8=
|
||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
|
||||
@@ -1080,8 +1080,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y=
|
||||
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
@@ -1265,8 +1265,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430 h1:qT0D7AIV0GRu8JUrSJYuyzj86kqLgksKQjwD++DqyOM=
|
||||
github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430/go.mod h1:3ER/8BhIEhvrddcztLQSc5ez1f1jNHIPdquc1F+DzOw=
|
||||
github.com/grafana/alerting v0.0.0-20250903141736-c9c007e7f0a8 h1:tXDlMjugyEbsrSJC2Np/9ZvBzEoa1xB+KGHUBnvPIFw=
|
||||
github.com/grafana/alerting v0.0.0-20250903141736-c9c007e7f0a8/go.mod h1:3ER/8BhIEhvrddcztLQSc5ez1f1jNHIPdquc1F+DzOw=
|
||||
github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d h1:TDVZemfYeJHPyXeYCnqL7BQqsa+mpaZYth/Qm3TKaT8=
|
||||
github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM=
|
||||
@@ -1281,8 +1281,8 @@ github.com/grafana/grafana-app-sdk v0.35.1 h1:zEXubzsQrxGBOzXJJMBwhEClC/tvPi0sfK
|
||||
github.com/grafana/grafana-app-sdk v0.35.1/go.mod h1:Zx5MkVppYK+ElSDUAR6+fjzOVo6I/cIgk+ty+LmNOxI=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.35.1 h1:taVpl+RoixTYl0JBJGhH+fPVmwA9wvdwdzJTZsv9buM=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.35.1/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk=
|
||||
github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX/Gh3FZKBE=
|
||||
github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM=
|
||||
github.com/grafana/grafana-aws-sdk v0.31.6-0.20250918141554-0b96cca5e46b h1:25XvbNSo8Sgi3MBFOtToRvlbXwfQ44nTb/NYCpZxUJE=
|
||||
github.com/grafana/grafana-aws-sdk v0.31.6-0.20250918141554-0b96cca5e46b/go.mod h1:2JJbVLc3jjWu0aAQx4bhPa2ChOUjLs9x7ime6mB4O+o=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.277.0 h1:VDU2F4Y5NeRS//ejctdZtsAshrGaEdbtW33FsK0EQss=
|
||||
|
||||
@@ -58,25 +58,25 @@ require (
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
|
||||
github.com/apache/arrow-go/v18 v18.2.0 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
||||
github.com/aws/smithy-go v1.20.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 // indirect
|
||||
github.com/aws/smithy-go v1.23.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bufbuild/protocompile v0.4.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
|
||||
@@ -70,44 +70,44 @@ github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
|
||||
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
|
||||
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
|
||||
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.0/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.8 h1:kQjtOLlTU4m4A64TsRcqwNChhGCwaPBt+zCQt/oWsHU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.8/go.mod h1:QPpc7IgljrKwH0+E6/KolCgr4WPLerURiU592AYzfSY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 h1:zmc9e1q90wMn8wQbjryy8IwA6Q4XlaL9Bx2zIqdNNbk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.12/go.mod h1:3VzdRDR5u3sSJRI4kYcOSIBbeYsgtVk7dG5R/U6qLWY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 h1:Is2tPmieqGS2edBnmOJIbdvOA6Op+rRpaYR60iBAwXM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7/go.mod h1:F1i5V5421EGci570yABvpIXgRIBPb5JM+lSkHF6Dq5w=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 h1:zeN9UtUlA6FTx0vFSayxSX32HDw73Yb6Hh2izDSFxXY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 h1:UCxq0X9O3xrlENdKf1r9eRJoKz/b0AfGkpp3a7FPlhg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7/go.mod h1:rHRoJUNUASj5Z/0eqI4w32vKvC7atoWR0jC+IkmVH8k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 h1:Y6DTZUn7ZUC4th9FMBbo8LVE+1fyq3ofw+tRwkUd3PY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7/go.mod h1:x3XE6vMnU9QvHN/Wrx2s44kwzV2o2g5x/siw4ZUJ9g8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 h1:mLgc5QIgOy26qyh5bvW+nDoAppxgn3J2WV3m9ewq7+8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7/go.mod h1:wXb/eQnqt8mDQIQTTmcw58B5mYGxzLGZGK8PWNFZ0BA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
|
||||
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
|
||||
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 h1:7PKX3VYsZ8LUWceVRuv0+PU+E7OtQb1lgmi5vmUE9CM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3/go.mod h1:Ql6jE9kyyWI5JHn+61UT/Y5Z0oyVJGmgmJbZD5g4unY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 h1:e0XBRn3AptQotkyBFrHAxFB8mDhAIOfsG+7KyJ0dg98=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4/go.mod h1:XclEty74bsGBCr1s0VSaA11hQ4ZidK4viWK7rRfO88I=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 h1:PR00NXRYgY4FWHqOGx3fC3lhVKjsp1GdloDv2ynMSd8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4/go.mod h1:Z+Gd23v97pX9zK97+tX4ppAgqCt3Z2dIXB02CtBncK8=
|
||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
|
||||
|
||||
@@ -1,24 +1,45 @@
|
||||
package routes
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsauth"
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newTestDatasource(opts ...func(*DataSource)) *DataSource {
|
||||
ds := &DataSource{
|
||||
AWSConfigProvider: awsauth.NewFakeConfigProvider(false),
|
||||
logger: log.NewNullLogger(),
|
||||
tagValueCache: cache.New(0, 0),
|
||||
Settings: models.CloudWatchSettings{
|
||||
AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "us-east-1"},
|
||||
},
|
||||
}
|
||||
ds.resourceHandler = httpadapter.New(ds.newResourceMux())
|
||||
for _, opt := range opts {
|
||||
opt(ds)
|
||||
}
|
||||
return ds
|
||||
}
|
||||
|
||||
func Test_accounts_route(t *testing.T) {
|
||||
origNewAccountsService := newAccountsService
|
||||
ds := newTestDatasource()
|
||||
origNewAccountsService := services.NewAccountsService
|
||||
t.Cleanup(func() {
|
||||
newAccountsService = origNewAccountsService
|
||||
services.NewAccountsService = origNewAccountsService
|
||||
})
|
||||
|
||||
t.Run("successfully returns array of accounts json", func(t *testing.T) {
|
||||
@@ -31,13 +52,13 @@ func Test_accounts_route(t *testing.T) {
|
||||
IsMonitoringAccount: true,
|
||||
},
|
||||
}}, nil)
|
||||
newAccountsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.AccountsProvider, error) {
|
||||
return &mockAccountsService, nil
|
||||
services.NewAccountsService = func(_ models.OAMAPIProvider) models.AccountsProvider {
|
||||
return &mockAccountsService
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/accounts?region=us-east-1", nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(AccountsHandler, logger, nil))
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.AccountsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
@@ -47,7 +68,7 @@ func Test_accounts_route(t *testing.T) {
|
||||
t.Run("rejects POST method", func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/accounts?region=us-east-1", nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(AccountsHandler, logger, nil))
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.AccountsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
|
||||
})
|
||||
@@ -55,7 +76,7 @@ func Test_accounts_route(t *testing.T) {
|
||||
t.Run("requires region query value", func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/accounts", nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(AccountsHandler, logger, nil))
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.AccountsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
})
|
||||
@@ -64,13 +85,13 @@ func Test_accounts_route(t *testing.T) {
|
||||
mockAccountsService := mocks.AccountsServiceMock{}
|
||||
mockAccountsService.On("GetAccountsForCurrentUserOrRole").Return([]resources.ResourceResponse[resources.Account](nil),
|
||||
fmt.Errorf("%w: %s", services.ErrAccessDeniedException, "some AWS message"))
|
||||
newAccountsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.AccountsProvider, error) {
|
||||
return &mockAccountsService, nil
|
||||
services.NewAccountsService = func(_ models.OAMAPIProvider) models.AccountsProvider {
|
||||
return &mockAccountsService
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/accounts?region=us-east-1", nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(AccountsHandler, logger, nil))
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.AccountsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
@@ -82,13 +103,13 @@ func Test_accounts_route(t *testing.T) {
|
||||
t.Run("returns 500 when accounts service returns unknown error", func(t *testing.T) {
|
||||
mockAccountsService := mocks.AccountsServiceMock{}
|
||||
mockAccountsService.On("GetAccountsForCurrentUserOrRole").Return([]resources.ResourceResponse[resources.Account](nil), fmt.Errorf("some error"))
|
||||
newAccountsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.AccountsProvider, error) {
|
||||
return &mockAccountsService, nil
|
||||
services.NewAccountsService = func(_ models.OAMAPIProvider) models.AccountsProvider {
|
||||
return &mockAccountsService
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/accounts?region=us-east-1", nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(AccountsHandler, logger, nil))
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.AccountsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
|
||||
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery"
|
||||
@@ -21,7 +23,7 @@ type annotationEvent struct {
|
||||
Text string
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginCtx backend.PluginContext, model DataQueryJson, query backend.DataQuery) (*backend.QueryDataResponse, error) {
|
||||
func (ds *DataSource) executeAnnotationQuery(ctx context.Context, model DataQueryJson, query backend.DataQuery) (*backend.QueryDataResponse, error) {
|
||||
result := backend.NewQueryDataResponse()
|
||||
statistic := ""
|
||||
|
||||
@@ -29,14 +31,14 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
|
||||
statistic = *model.Statistic
|
||||
}
|
||||
|
||||
var period int64
|
||||
var period int32
|
||||
|
||||
if model.Period != nil && *model.Period != "" {
|
||||
p, err := strconv.ParseInt(*model.Period, 10, 64)
|
||||
p, err := strconv.ParseInt(*model.Period, 10, 32)
|
||||
if err != nil {
|
||||
return nil, backend.DownstreamError(fmt.Errorf("query period must be an int"))
|
||||
}
|
||||
period = p
|
||||
period = int32(p)
|
||||
}
|
||||
|
||||
prefixMatching := false
|
||||
@@ -50,7 +52,7 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
|
||||
actionPrefix := model.ActionPrefix
|
||||
alarmNamePrefix := model.AlarmNamePrefix
|
||||
|
||||
cli, err := e.getCWClient(ctx, pluginCtx, model.Region)
|
||||
cli, err := ds.getCWClient(ctx, model.Region)
|
||||
if err != nil {
|
||||
result.Responses[query.RefID] = backend.ErrorResponseWithErrorSource(fmt.Errorf("%v: %w", "failed to get client", err))
|
||||
return result, nil
|
||||
@@ -69,11 +71,11 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
|
||||
|
||||
if prefixMatching {
|
||||
params := &cloudwatch.DescribeAlarmsInput{
|
||||
MaxRecords: aws.Int64(100),
|
||||
MaxRecords: aws.Int32(100),
|
||||
ActionPrefix: actionPrefix,
|
||||
AlarmNamePrefix: alarmNamePrefix,
|
||||
}
|
||||
resp, err := cli.DescribeAlarms(params)
|
||||
resp, err := cli.DescribeAlarms(ctx, params)
|
||||
if err != nil {
|
||||
result.Responses[query.RefID] = backend.ErrorResponseWithErrorSource(backend.DownstreamError(fmt.Errorf("%v: %w", "failed to call cloudwatch:DescribeAlarms", err)))
|
||||
return result, nil
|
||||
@@ -84,10 +86,10 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
|
||||
return result, backend.DownstreamError(errors.New("invalid annotations query"))
|
||||
}
|
||||
|
||||
var qd []*cloudwatch.Dimension
|
||||
var qd []cloudwatchtypes.Dimension
|
||||
for k, v := range dimensions {
|
||||
for _, vvv := range v.ArrayOfString {
|
||||
qd = append(qd, &cloudwatch.Dimension{
|
||||
qd = append(qd, cloudwatchtypes.Dimension{
|
||||
Name: aws.String(k),
|
||||
Value: aws.String(vvv),
|
||||
})
|
||||
@@ -97,10 +99,10 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
|
||||
Namespace: aws.String(model.Namespace),
|
||||
MetricName: aws.String(metricName),
|
||||
Dimensions: qd,
|
||||
Statistic: aws.String(statistic),
|
||||
Period: aws.Int64(period),
|
||||
Statistic: cloudwatchtypes.Statistic(statistic),
|
||||
Period: aws.Int32(period),
|
||||
}
|
||||
resp, err := cli.DescribeAlarmsForMetric(params)
|
||||
resp, err := cli.DescribeAlarmsForMetric(ctx, params)
|
||||
if err != nil {
|
||||
result.Responses[query.RefID] = backend.ErrorResponseWithErrorSource(backend.DownstreamError(fmt.Errorf("%v: %w", "failed to call cloudwatch:DescribeAlarmsForMetric", err)))
|
||||
return result, nil
|
||||
@@ -116,9 +118,9 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
|
||||
AlarmName: alarmName,
|
||||
StartDate: aws.Time(query.TimeRange.From),
|
||||
EndDate: aws.Time(query.TimeRange.To),
|
||||
MaxRecords: aws.Int64(100),
|
||||
MaxRecords: aws.Int32(100),
|
||||
}
|
||||
resp, err := cli.DescribeAlarmHistory(params)
|
||||
resp, err := cli.DescribeAlarmHistory(ctx, params)
|
||||
if err != nil {
|
||||
result.Responses[query.RefID] = backend.ErrorResponseWithErrorSource(backend.DownstreamError(fmt.Errorf("%v: %w", "failed to call cloudwatch:DescribeAlarmHistory", err)))
|
||||
return result, nil
|
||||
@@ -127,7 +129,7 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
|
||||
annotations = append(annotations, &annotationEvent{
|
||||
Time: *history.Timestamp,
|
||||
Title: *history.AlarmName,
|
||||
Tags: *history.HistoryItemType,
|
||||
Tags: string(history.HistoryItemType),
|
||||
Text: *history.HistorySummary,
|
||||
})
|
||||
}
|
||||
@@ -162,7 +164,7 @@ func transformAnnotationToTable(annotations []*annotationEvent, query backend.Da
|
||||
}
|
||||
|
||||
func filterAlarms(alarms *cloudwatch.DescribeAlarmsOutput, namespace string, metricName string,
|
||||
dimensions dataquery.Dimensions, statistic string, period int64) []*string {
|
||||
dimensions dataquery.Dimensions, statistic string, period int32) []*string {
|
||||
alarmNames := make([]*string, 0)
|
||||
|
||||
for _, alarm := range alarms.MetricAlarms {
|
||||
@@ -189,7 +191,7 @@ func filterAlarms(alarms *cloudwatch.DescribeAlarmsOutput, namespace string, met
|
||||
continue
|
||||
}
|
||||
|
||||
if *alarm.Statistic != statistic {
|
||||
if string(alarm.Statistic) != statistic {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -5,33 +5,31 @@ import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQuery_AnnotationQuery(t *testing.T) {
|
||||
ds := newTestDatasource()
|
||||
origNewCWClient := NewCWClient
|
||||
t.Cleanup(func() {
|
||||
NewCWClient = origNewCWClient
|
||||
})
|
||||
|
||||
var client fakeCWAnnotationsClient
|
||||
NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI {
|
||||
NewCWClient = func(aws.Config) models.CWClient {
|
||||
return &client
|
||||
}
|
||||
|
||||
t.Run("DescribeAlarmsForMetric is called with minimum parameters", func(t *testing.T) {
|
||||
client = fakeCWAnnotationsClient{describeAlarmsForMetricOutput: &cloudwatch.DescribeAlarmsForMetricOutput{}}
|
||||
im := defaultTestInstanceManager()
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
|
||||
},
|
||||
@@ -53,17 +51,15 @@ func TestQuery_AnnotationQuery(t *testing.T) {
|
||||
assert.Equal(t, &cloudwatch.DescribeAlarmsForMetricInput{
|
||||
Namespace: aws.String("custom"),
|
||||
MetricName: aws.String("CPUUtilization"),
|
||||
Statistic: aws.String("Average"),
|
||||
Period: aws.Int64(300),
|
||||
Statistic: "Average",
|
||||
Period: aws.Int32(300),
|
||||
}, client.calls.describeAlarmsForMetric[0])
|
||||
})
|
||||
|
||||
t.Run("DescribeAlarms is called when prefixMatching is true", func(t *testing.T) {
|
||||
client = fakeCWAnnotationsClient{describeAlarmsOutput: &cloudwatch.DescribeAlarmsOutput{}}
|
||||
im := defaultTestInstanceManager()
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
|
||||
},
|
||||
@@ -86,7 +82,7 @@ func TestQuery_AnnotationQuery(t *testing.T) {
|
||||
|
||||
require.Len(t, client.calls.describeAlarms, 1)
|
||||
assert.Equal(t, &cloudwatch.DescribeAlarmsInput{
|
||||
MaxRecords: aws.Int64(100),
|
||||
MaxRecords: aws.Int32(100),
|
||||
ActionPrefix: aws.String("some_action_prefix"),
|
||||
AlarmNamePrefix: aws.String("some_alarm_name_prefix"),
|
||||
}, client.calls.describeAlarms[0])
|
||||
|
||||
@@ -1,64 +1,53 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws/client"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/oam"
|
||||
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
|
||||
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
"github.com/aws/aws-sdk-go-v2/service/oam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
)
|
||||
|
||||
// NewMetricsAPI is a CloudWatch metrics api factory.
|
||||
// NewCWClient is a CloudWatch metrics api factory.
|
||||
//
|
||||
// Stubbable by tests.
|
||||
var NewMetricsAPI = func(sess *session.Session) models.CloudWatchMetricsAPIProvider {
|
||||
return cloudwatch.New(sess)
|
||||
var NewCWClient = func(cfg aws.Config) models.CWClient {
|
||||
return cloudwatch.NewFromConfig(cfg)
|
||||
}
|
||||
|
||||
// NewLogsAPI is a CloudWatch logs api factory.
|
||||
//
|
||||
// Stubbable by tests.
|
||||
var NewLogsAPI = func(sess *session.Session) models.CloudWatchLogsAPIProvider {
|
||||
return cloudwatchlogs.New(sess)
|
||||
var NewLogsAPI = func(cfg aws.Config) models.CloudWatchLogsAPIProvider {
|
||||
return cloudwatchlogs.NewFromConfig(cfg)
|
||||
}
|
||||
|
||||
// NewOAMAPI is a CloudWatch OAM api factory.
|
||||
// NewOAMAPI is a CloudWatch OAM API factory
|
||||
//
|
||||
// Stubbable by tests.
|
||||
var NewOAMAPI = func(sess *session.Session) models.OAMAPIProvider {
|
||||
return oam.New(sess)
|
||||
var NewOAMAPI = func(cfg aws.Config) models.OAMAPIProvider {
|
||||
return oam.NewFromConfig(cfg)
|
||||
}
|
||||
|
||||
// NewCWClient is a CloudWatch client factory.
|
||||
// NewEC2API is a CloudWatch EC2 API factory
|
||||
//
|
||||
// Stubbable by tests.
|
||||
var NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI {
|
||||
return cloudwatch.New(sess)
|
||||
// Stubbable by tests
|
||||
var NewEC2API = func(cfg aws.Config) models.EC2APIProvider {
|
||||
return ec2.NewFromConfig(cfg)
|
||||
}
|
||||
|
||||
// NewCWLogsClient is a CloudWatch logs client factory.
|
||||
//
|
||||
// Stubbable by tests.
|
||||
var NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
return cloudwatchlogs.New(sess)
|
||||
var NewCWLogsClient = func(cfg aws.Config) models.CWLogsClient {
|
||||
return cloudwatchlogs.NewFromConfig(cfg)
|
||||
}
|
||||
|
||||
// NewEC2Client is a client factory.
|
||||
// NewRGTAClient is a ResourceGroupsTaggingAPI Client factory.
|
||||
//
|
||||
// Stubbable by tests.
|
||||
var NewEC2Client = func(provider client.ConfigProvider) models.EC2APIProvider {
|
||||
return ec2.New(provider)
|
||||
}
|
||||
|
||||
// RGTA client factory.
|
||||
//
|
||||
// Stubbable by tests.
|
||||
var newRGTAClient = func(provider client.ConfigProvider) resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI {
|
||||
return resourcegroupstaggingapi.New(provider)
|
||||
var NewRGTAClient = func(cfg aws.Config) resourcegroupstaggingapi.GetResourcesAPIClient {
|
||||
return resourcegroupstaggingapi.NewFromConfig(cfg)
|
||||
}
|
||||
|
||||
@@ -3,41 +3,42 @@ package clients
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/awsutil"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
|
||||
)
|
||||
|
||||
// this client wraps the CloudWatch API and handles pagination and the composition of the MetricResponse DTO
|
||||
type metricsClient struct {
|
||||
models.CloudWatchMetricsAPIProvider
|
||||
type MetricsClient struct {
|
||||
cloudwatch.ListMetricsAPIClient
|
||||
|
||||
listMetricsPageLimit int
|
||||
}
|
||||
|
||||
func NewMetricsClient(api models.CloudWatchMetricsAPIProvider, pageLimit int) *metricsClient {
|
||||
return &metricsClient{CloudWatchMetricsAPIProvider: api, listMetricsPageLimit: pageLimit}
|
||||
func NewMetricsClient(client cloudwatch.ListMetricsAPIClient, listMetricsPageLimit int) *MetricsClient {
|
||||
return &MetricsClient{
|
||||
ListMetricsAPIClient: client,
|
||||
listMetricsPageLimit: listMetricsPageLimit,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *metricsClient) ListMetricsWithPageLimit(ctx context.Context, params *cloudwatch.ListMetricsInput) ([]resources.MetricResponse, error) {
|
||||
var cloudWatchMetrics []resources.MetricResponse
|
||||
pageNum := 0
|
||||
err := l.ListMetricsPagesWithContext(ctx, params, func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
|
||||
pageNum++
|
||||
utils.QueriesTotalCounter.WithLabelValues(utils.ListMetricsLabel).Inc()
|
||||
metrics, err := awsutil.ValuesAtPath(page, "Metrics")
|
||||
if err == nil {
|
||||
for idx, metric := range metrics {
|
||||
metric := resources.MetricResponse{Metric: metric.(*cloudwatch.Metric)}
|
||||
if len(page.OwningAccounts) >= idx && params.IncludeLinkedAccounts != nil && *params.IncludeLinkedAccounts {
|
||||
metric.AccountId = page.OwningAccounts[idx]
|
||||
}
|
||||
cloudWatchMetrics = append(cloudWatchMetrics, metric)
|
||||
}
|
||||
func (mc *MetricsClient) ListMetricsWithPageLimit(ctx context.Context, params *cloudwatch.ListMetricsInput) ([]resources.MetricResponse, error) {
|
||||
var responses []resources.MetricResponse
|
||||
paginator := cloudwatch.NewListMetricsPaginator(mc.ListMetricsAPIClient, params)
|
||||
includeAccount := params.IncludeLinkedAccounts != nil && *params.IncludeLinkedAccounts
|
||||
pages := 0
|
||||
for paginator.HasMorePages() && pages < mc.listMetricsPageLimit {
|
||||
pages += 1
|
||||
page, err := paginator.NextPage(ctx)
|
||||
if err != nil {
|
||||
return responses, err
|
||||
}
|
||||
return !lastPage && pageNum < l.listMetricsPageLimit
|
||||
})
|
||||
|
||||
return cloudWatchMetrics, err
|
||||
for i, metric := range page.Metrics {
|
||||
resp := resources.MetricResponse{Metric: metric}
|
||||
if includeAccount && len(page.OwningAccounts) >= i {
|
||||
resp.AccountId = &page.OwningAccounts[i]
|
||||
}
|
||||
responses = append(responses, resp)
|
||||
}
|
||||
}
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
@@ -4,16 +4,19 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
|
||||
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMetricsClient(t *testing.T) {
|
||||
metrics := []*cloudwatch.Metric{
|
||||
metrics := []cloudwatchtypes.Metric{
|
||||
{MetricName: aws.String("Test_MetricName1")},
|
||||
{MetricName: aws.String("Test_MetricName2")},
|
||||
{MetricName: aws.String("Test_MetricName3")},
|
||||
@@ -50,25 +53,25 @@ func TestMetricsClient(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Should return account id in case IncludeLinkedAccounts is set to true", func(t *testing.T) {
|
||||
fakeApi := &mocks.FakeMetricsAPI{Metrics: []*cloudwatch.Metric{
|
||||
fakeApi := &mocks.FakeMetricsAPI{Metrics: []cloudwatchtypes.Metric{
|
||||
{MetricName: aws.String("Test_MetricName1")},
|
||||
{MetricName: aws.String("Test_MetricName2")},
|
||||
{MetricName: aws.String("Test_MetricName3")},
|
||||
}, OwningAccounts: []*string{aws.String("1234567890"), aws.String("1234567890"), aws.String("1234567895")}}
|
||||
}, OwningAccounts: []string{"1234567890", "1234567890", "1234567895"}}
|
||||
client := NewMetricsClient(fakeApi, 100)
|
||||
|
||||
response, err := client.ListMetricsWithPageLimit(ctx, &cloudwatch.ListMetricsInput{IncludeLinkedAccounts: aws.Bool(true)})
|
||||
require.NoError(t, err)
|
||||
expected := []resources.MetricResponse{
|
||||
{Metric: &cloudwatch.Metric{MetricName: aws.String("Test_MetricName1")}, AccountId: stringPtr("1234567890")},
|
||||
{Metric: &cloudwatch.Metric{MetricName: aws.String("Test_MetricName2")}, AccountId: stringPtr("1234567890")},
|
||||
{Metric: &cloudwatch.Metric{MetricName: aws.String("Test_MetricName3")}, AccountId: stringPtr("1234567895")},
|
||||
{Metric: cloudwatchtypes.Metric{MetricName: aws.String("Test_MetricName1")}, AccountId: stringPtr("1234567890")},
|
||||
{Metric: cloudwatchtypes.Metric{MetricName: aws.String("Test_MetricName2")}, AccountId: stringPtr("1234567890")},
|
||||
{Metric: cloudwatchtypes.Metric{MetricName: aws.String("Test_MetricName3")}, AccountId: stringPtr("1234567895")},
|
||||
}
|
||||
assert.Equal(t, expected, response)
|
||||
})
|
||||
|
||||
t.Run("Should not return account id in case IncludeLinkedAccounts is set to false", func(t *testing.T) {
|
||||
fakeApi := &mocks.FakeMetricsAPI{Metrics: []*cloudwatch.Metric{{MetricName: aws.String("Test_MetricName1")}}, OwningAccounts: []*string{aws.String("1234567890")}}
|
||||
fakeApi := &mocks.FakeMetricsAPI{Metrics: []cloudwatchtypes.Metric{{MetricName: aws.String("Test_MetricName1")}}, OwningAccounts: []string{"1234567890"}}
|
||||
client := NewMetricsClient(fakeApi, 100)
|
||||
|
||||
response, err := client.ListMetricsWithPageLimit(ctx, &cloudwatch.ListMetricsInput{IncludeLinkedAccounts: aws.Bool(false)})
|
||||
|
||||
@@ -3,22 +3,19 @@ package cloudwatch
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
||||
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
|
||||
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
|
||||
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsauth"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
|
||||
@@ -37,22 +34,7 @@ const (
|
||||
|
||||
// headerFromAlert is used by datasources to identify alert queries
|
||||
headerFromAlert = "FromAlert"
|
||||
)
|
||||
|
||||
type DataQueryJson struct {
|
||||
dataquery.CloudWatchAnnotationQuery
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
type DataSource struct {
|
||||
Settings models.CloudWatchSettings
|
||||
HTTPClient *http.Client
|
||||
sessions SessionCache
|
||||
tagValueCache *cache.Cache
|
||||
ProxyOpts *proxy.Options
|
||||
}
|
||||
|
||||
const (
|
||||
defaultRegion = "default"
|
||||
logsQueryMode = "Logs"
|
||||
// QueryTypes
|
||||
@@ -61,74 +43,72 @@ const (
|
||||
timeSeriesQuery = "timeSeriesQuery"
|
||||
)
|
||||
|
||||
func ProvideService(httpClientProvider *httpclient.Provider) *CloudWatchService {
|
||||
logger := backend.NewLoggerWith("logger", "tsdb.cloudwatch")
|
||||
logger.Debug("Initializing")
|
||||
|
||||
executor := newExecutor(
|
||||
datasource.NewInstanceManager(NewInstanceSettings(httpClientProvider)),
|
||||
logger,
|
||||
)
|
||||
|
||||
return &CloudWatchService{
|
||||
Executor: executor,
|
||||
}
|
||||
type DataQueryJson struct {
|
||||
dataquery.CloudWatchAnnotationQuery
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
type CloudWatchService struct {
|
||||
Executor *cloudWatchExecutor
|
||||
}
|
||||
|
||||
type SessionCache interface {
|
||||
GetSessionWithAuthSettings(c awsds.GetSessionConfig, as awsds.AuthSettings) (*session.Session, error)
|
||||
}
|
||||
|
||||
func newExecutor(im instancemgmt.InstanceManager, logger log.Logger) *cloudWatchExecutor {
|
||||
e := &cloudWatchExecutor{
|
||||
im: im,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
e.resourceHandler = httpadapter.New(e.newResourceMux())
|
||||
return e
|
||||
}
|
||||
|
||||
func NewInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc {
|
||||
return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
instanceSettings, err := models.LoadCloudWatchSettings(ctx, settings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading settings: %w", err)
|
||||
}
|
||||
|
||||
opts, err := settings.HTTPClientOptions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpClient, err := httpClientProvider.New(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating http client: %w", err)
|
||||
}
|
||||
|
||||
return DataSource{
|
||||
Settings: instanceSettings,
|
||||
HTTPClient: httpClient,
|
||||
tagValueCache: cache.New(tagValueCacheExpiration, tagValueCacheExpiration*5),
|
||||
sessions: awsds.NewSessionCache(),
|
||||
// this is used to build a custom dialer when secure socks proxy is enabled
|
||||
ProxyOpts: opts.ProxyOptions,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// cloudWatchExecutor executes CloudWatch requests
|
||||
type cloudWatchExecutor struct {
|
||||
im instancemgmt.InstanceManager
|
||||
logger log.Logger
|
||||
type DataSource struct {
|
||||
Settings models.CloudWatchSettings
|
||||
ProxyOpts *proxy.Options
|
||||
AWSConfigProvider awsauth.ConfigProvider
|
||||
|
||||
logger log.Logger
|
||||
tagValueCache *cache.Cache
|
||||
resourceHandler backend.CallResourceHandler
|
||||
}
|
||||
|
||||
func (ds *DataSource) newAWSConfig(ctx context.Context, region string) (aws.Config, error) {
|
||||
if region == defaultRegion || region == "" {
|
||||
if len(ds.Settings.Region) == 0 {
|
||||
return aws.Config{}, models.ErrMissingRegion
|
||||
}
|
||||
region = ds.Settings.Region
|
||||
}
|
||||
authSettings := awsauth.Settings{
|
||||
CredentialsProfile: ds.Settings.Profile,
|
||||
LegacyAuthType: ds.Settings.AuthType,
|
||||
AssumeRoleARN: ds.Settings.AssumeRoleARN,
|
||||
ExternalID: ds.Settings.ExternalID,
|
||||
Endpoint: ds.Settings.Endpoint,
|
||||
Region: region,
|
||||
AccessKey: ds.Settings.AccessKey,
|
||||
SecretKey: ds.Settings.SecretKey,
|
||||
HTTPClient: &http.Client{},
|
||||
}
|
||||
if ds.Settings.GrafanaSettings.SecureSocksDSProxyEnabled && ds.Settings.SecureSocksProxyEnabled {
|
||||
authSettings.ProxyOptions = ds.ProxyOpts
|
||||
}
|
||||
cfg, err := ds.AWSConfigProvider.GetConfig(ctx, authSettings)
|
||||
if err != nil {
|
||||
return aws.Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func NewDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
instanceSettings, err := models.LoadCloudWatchSettings(ctx, settings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading settings: %w", err)
|
||||
}
|
||||
|
||||
opts, err := settings.HTTPClientOptions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ds := &DataSource{
|
||||
Settings: instanceSettings,
|
||||
// this is used to build a custom dialer when secure socks proxy is enabled
|
||||
ProxyOpts: opts.ProxyOptions,
|
||||
AWSConfigProvider: awsauth.NewConfigProvider(),
|
||||
logger: backend.NewLoggerWith("logger", "grafana-cloudwatch-datasource"),
|
||||
tagValueCache: cache.New(tagValueCacheExpiration, tagValueCacheExpiration*5),
|
||||
}
|
||||
ds.resourceHandler = httpadapter.New(ds.newResourceMux())
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// instrumentContext adds plugin key-values to the context; later, logger.FromContext(ctx) will provide a logger
|
||||
// that adds these values to its output.
|
||||
// TODO: move this into the sdk (see https://github.com/grafana/grafana/issues/82033)
|
||||
@@ -144,59 +124,12 @@ func instrumentContext(ctx context.Context, endpoint string, pCtx backend.Plugin
|
||||
return log.WithContextualAttributes(ctx, p)
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) getRequestContext(ctx context.Context, pluginCtx backend.PluginContext, region string) (models.RequestContext, error) {
|
||||
r := region
|
||||
instance, err := e.getInstance(ctx, pluginCtx)
|
||||
if region == defaultRegion {
|
||||
if err != nil {
|
||||
return models.RequestContext{}, err
|
||||
}
|
||||
r = instance.Settings.Region
|
||||
}
|
||||
|
||||
ec2Client, err := e.getEC2Client(ctx, pluginCtx, defaultRegion)
|
||||
if err != nil {
|
||||
return models.RequestContext{}, err
|
||||
}
|
||||
|
||||
sess, err := instance.newSession(r)
|
||||
if err != nil {
|
||||
return models.RequestContext{}, err
|
||||
}
|
||||
|
||||
return models.RequestContext{
|
||||
OAMAPIProvider: NewOAMAPI(sess),
|
||||
MetricsClientProvider: clients.NewMetricsClient(NewMetricsAPI(sess), instance.Settings.GrafanaSettings.ListMetricsPageLimit),
|
||||
LogsAPIProvider: NewLogsAPI(sess),
|
||||
EC2APIProvider: ec2Client,
|
||||
Settings: instance.Settings,
|
||||
Logger: e.logger.FromContext(ctx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getRequestContextOnlySettings is useful for resource endpoints that are called before auth has been configured such as external-id that need access to settings but nothing else
|
||||
func (e *cloudWatchExecutor) getRequestContextOnlySettings(ctx context.Context, pluginCtx backend.PluginContext, _ string) (models.RequestContext, error) {
|
||||
instance, err := e.getInstance(ctx, pluginCtx)
|
||||
if err != nil {
|
||||
return models.RequestContext{}, err
|
||||
}
|
||||
|
||||
return models.RequestContext{
|
||||
OAMAPIProvider: nil,
|
||||
MetricsClientProvider: nil,
|
||||
LogsAPIProvider: nil,
|
||||
EC2APIProvider: nil,
|
||||
Settings: instance.Settings,
|
||||
Logger: e.logger.FromContext(ctx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
func (ds *DataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
ctx = instrumentContext(ctx, string(backend.EndpointCallResource), req.PluginContext)
|
||||
return e.resourceHandler.CallResource(ctx, req, sender)
|
||||
return ds.resourceHandler.CallResource(ctx, req, sender)
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
func (ds *DataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
ctx = instrumentContext(ctx, string(backend.EndpointQueryData), req.PluginContext)
|
||||
q := req.Queries[0]
|
||||
var model DataQueryJson
|
||||
@@ -217,37 +150,37 @@ func (e *cloudWatchExecutor) QueryData(ctx context.Context, req *backend.QueryDa
|
||||
fromPublicDashboard := model.Type == "" && queryMode == logsQueryMode
|
||||
isSyncLogQuery := ((fromAlert || fromExpression) && queryMode == logsQueryMode) || fromPublicDashboard
|
||||
if isSyncLogQuery {
|
||||
return executeSyncLogQuery(ctx, e, req)
|
||||
return executeSyncLogQuery(ctx, ds, req)
|
||||
}
|
||||
|
||||
var result *backend.QueryDataResponse
|
||||
switch model.Type {
|
||||
case annotationQuery:
|
||||
result, err = e.executeAnnotationQuery(ctx, req.PluginContext, model, q)
|
||||
result, err = ds.executeAnnotationQuery(ctx, model, q)
|
||||
case logAction:
|
||||
result, err = e.executeLogActions(ctx, req)
|
||||
result, err = ds.executeLogActions(ctx, req)
|
||||
case timeSeriesQuery:
|
||||
fallthrough
|
||||
default:
|
||||
result, err = e.executeTimeSeriesQuery(ctx, req)
|
||||
result, err = ds.executeTimeSeriesQuery(ctx, req)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
func (ds *DataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
ctx = instrumentContext(ctx, string(backend.EndpointCheckHealth), req.PluginContext)
|
||||
status := backend.HealthStatusOk
|
||||
metricsTest := "Successfully queried the CloudWatch metrics API."
|
||||
logsTest := "Successfully queried the CloudWatch logs API."
|
||||
|
||||
err := e.checkHealthMetrics(ctx, req.PluginContext)
|
||||
err := ds.checkHealthMetrics(ctx, req.PluginContext)
|
||||
if err != nil {
|
||||
status = backend.HealthStatusError
|
||||
metricsTest = fmt.Sprintf("CloudWatch metrics query failed: %s", err.Error())
|
||||
}
|
||||
|
||||
err = e.checkHealthLogs(ctx, req.PluginContext)
|
||||
err = ds.checkHealthLogs(ctx)
|
||||
if err != nil {
|
||||
status = backend.HealthStatusError
|
||||
logsTest = fmt.Sprintf("CloudWatch logs query failed: %s", err.Error())
|
||||
@@ -259,7 +192,7 @@ func (e *cloudWatchExecutor) CheckHealth(ctx context.Context, req *backend.Check
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) checkHealthMetrics(ctx context.Context, pluginCtx backend.PluginContext) error {
|
||||
func (ds *DataSource) checkHealthMetrics(ctx context.Context, _ backend.PluginContext) error {
|
||||
namespace := "AWS/Billing"
|
||||
metric := "EstimatedCharges"
|
||||
params := &cloudwatch.ListMetricsInput{
|
||||
@@ -267,141 +200,75 @@ func (e *cloudWatchExecutor) checkHealthMetrics(ctx context.Context, pluginCtx b
|
||||
MetricName: &metric,
|
||||
}
|
||||
|
||||
instance, err := e.getInstance(ctx, pluginCtx)
|
||||
cfg, err := ds.newAWSConfig(ctx, defaultRegion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := instance.newSession(defaultRegion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metricClient := clients.NewMetricsClient(NewMetricsAPI(session), instance.Settings.GrafanaSettings.ListMetricsPageLimit)
|
||||
metricClient := clients.NewMetricsClient(NewCWClient(cfg), ds.Settings.GrafanaSettings.ListMetricsPageLimit)
|
||||
_, err = metricClient.ListMetricsWithPageLimit(ctx, params)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) checkHealthLogs(ctx context.Context, pluginCtx backend.PluginContext) error {
|
||||
session, err := e.newSessionFromContext(ctx, pluginCtx, defaultRegion)
|
||||
func (ds *DataSource) checkHealthLogs(ctx context.Context) error {
|
||||
cfg, err := ds.getAWSConfig(ctx, defaultRegion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logsClient := NewLogsAPI(session)
|
||||
_, err = logsClient.DescribeLogGroupsWithContext(ctx, &cloudwatchlogs.DescribeLogGroupsInput{Limit: aws.Int64(1)})
|
||||
logsClient := NewLogsAPI(cfg)
|
||||
_, err = logsClient.DescribeLogGroups(ctx, &cloudwatchlogs.DescribeLogGroupsInput{Limit: aws.Int32(1)})
|
||||
return err
|
||||
}
|
||||
|
||||
func (ds *DataSource) newSession(region string) (*session.Session, error) {
|
||||
if region == defaultRegion {
|
||||
if len(ds.Settings.Region) == 0 {
|
||||
return nil, models.ErrMissingRegion
|
||||
}
|
||||
region = ds.Settings.Region
|
||||
}
|
||||
sess, err := ds.sessions.GetSessionWithAuthSettings(awsds.GetSessionConfig{
|
||||
// https://github.com/grafana/grafana/issues/46365
|
||||
// HTTPClient: instance.HTTPClient,
|
||||
Settings: awsds.AWSDatasourceSettings{
|
||||
Profile: ds.Settings.Profile,
|
||||
Region: region,
|
||||
AuthType: ds.Settings.AuthType,
|
||||
AssumeRoleARN: ds.Settings.AssumeRoleARN,
|
||||
ExternalID: ds.Settings.ExternalID,
|
||||
Endpoint: ds.Settings.Endpoint,
|
||||
DefaultRegion: ds.Settings.Region,
|
||||
AccessKey: ds.Settings.AccessKey,
|
||||
SecretKey: ds.Settings.SecretKey,
|
||||
},
|
||||
UserAgentName: aws.String("Cloudwatch")},
|
||||
ds.Settings.GrafanaSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// work around until https://github.com/grafana/grafana/issues/39089 is implemented
|
||||
if ds.Settings.GrafanaSettings.SecureSocksDSProxyEnabled && ds.Settings.SecureSocksProxyEnabled {
|
||||
// only update the transport to try to avoid the issue mentioned here https://github.com/grafana/grafana/issues/46365
|
||||
// also, 'sess' is cached and reused, so the first time it might have the transport not set, the following uses it will
|
||||
if sess.Config.HTTPClient.Transport == nil {
|
||||
// following go standard library logic (https://pkg.go.dev/net/http#Client), if no Transport is provided,
|
||||
// then we use http.DefaultTransport
|
||||
defTransport, ok := http.DefaultTransport.(*http.Transport)
|
||||
if !ok {
|
||||
// this should not happen but validating just in case
|
||||
return nil, errors.New("default http client transport is not of type http.Transport")
|
||||
}
|
||||
sess.Config.HTTPClient.Transport = defTransport.Clone()
|
||||
}
|
||||
err = proxy.New(ds.ProxyOpts).ConfigureSecureSocksHTTPProxy(sess.Config.HTTPClient.Transport.(*http.Transport))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error configuring Secure Socks proxy for Transport: %w", err)
|
||||
}
|
||||
} else if sess.Config.HTTPClient != nil {
|
||||
// Workaround for https://github.com/grafana/grafana/issues/91356 - PDC transport set above
|
||||
// stays on the cached session after PDC is disabled
|
||||
sess.Config.HTTPClient.Transport = nil
|
||||
}
|
||||
return sess, nil
|
||||
func (ds *DataSource) getAWSConfig(ctx context.Context, region string) (aws.Config, error) {
|
||||
return ds.newAWSConfig(ctx, region)
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) newSessionFromContext(ctx context.Context, pluginCtx backend.PluginContext, region string) (*session.Session, error) {
|
||||
instance, err := e.getInstance(ctx, pluginCtx)
|
||||
func (ds *DataSource) getCWClient(ctx context.Context, region string) (models.CWClient, error) {
|
||||
cfg, err := ds.getAWSConfig(ctx, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return instance.newSession(region)
|
||||
return NewCWClient(cfg), nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) getInstance(ctx context.Context, pluginCtx backend.PluginContext) (*DataSource, error) {
|
||||
i, err := e.im.Get(ctx, pluginCtx)
|
||||
func (ds *DataSource) getCWLogsClient(ctx context.Context, region string) (models.CWLogsClient, error) {
|
||||
cfg, err := ds.getAWSConfig(ctx, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instance := i.(DataSource)
|
||||
return &instance, nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) getCWClient(ctx context.Context, pluginCtx backend.PluginContext, region string) (cloudwatchiface.CloudWatchAPI, error) {
|
||||
sess, err := e.newSessionFromContext(ctx, pluginCtx, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewCWClient(sess), nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) getCWLogsClient(ctx context.Context, pluginCtx backend.PluginContext, region string) (cloudwatchlogsiface.CloudWatchLogsAPI, error) {
|
||||
sess, err := e.newSessionFromContext(ctx, pluginCtx, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logsClient := NewCWLogsClient(sess)
|
||||
logsClient := NewCWLogsClient(cfg)
|
||||
|
||||
return logsClient, nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) getEC2Client(ctx context.Context, pluginCtx backend.PluginContext, region string) (models.EC2APIProvider, error) {
|
||||
sess, err := e.newSessionFromContext(ctx, pluginCtx, region)
|
||||
func (ds *DataSource) getEC2Client(ctx context.Context, region string) (models.EC2APIProvider, error) {
|
||||
cfg, err := ds.getAWSConfig(ctx, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewEC2Client(sess), nil
|
||||
return NewEC2API(cfg), nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) getRGTAClient(ctx context.Context, pluginCtx backend.PluginContext, region string) (resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI,
|
||||
func (ds *DataSource) getRGTAClient(ctx context.Context, region string) (resourcegroupstaggingapi.GetResourcesAPIClient,
|
||||
error) {
|
||||
sess, err := e.newSessionFromContext(ctx, pluginCtx, region)
|
||||
cfg, err := ds.getAWSConfig(ctx, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newRGTAClient(sess), nil
|
||||
return NewRGTAClient(cfg), nil
|
||||
}
|
||||
|
||||
func isTerminated(queryStatus string) bool {
|
||||
return queryStatus == "Complete" || queryStatus == "Cancelled" || queryStatus == "Failed" || queryStatus == "Timeout"
|
||||
var terminatedStates = []cloudwatchlogstypes.QueryStatus{
|
||||
cloudwatchlogstypes.QueryStatusComplete,
|
||||
cloudwatchlogstypes.QueryStatusCancelled,
|
||||
cloudwatchlogstypes.QueryStatusFailed,
|
||||
cloudwatchlogstypes.QueryStatusTimeout,
|
||||
}
|
||||
|
||||
func isTerminated(queryStatus cloudwatchlogstypes.QueryStatus) bool {
|
||||
return slices.Contains(terminatedStates, queryStatus)
|
||||
}
|
||||
|
||||
@@ -6,17 +6,13 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/client"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
|
||||
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
|
||||
@@ -27,48 +23,49 @@ import (
|
||||
|
||||
func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
sender := &mockedCallResourceResponseSenderForOauth{}
|
||||
origNewMetricsAPI := NewMetricsAPI
|
||||
origNewCWClient := NewCWClient
|
||||
origNewOAMAPI := NewOAMAPI
|
||||
origNewLogsAPI := NewLogsAPI
|
||||
origNewEC2Client := NewEC2Client
|
||||
NewOAMAPI = func(sess *session.Session) models.OAMAPIProvider { return nil }
|
||||
origNewEC2API := NewEC2API
|
||||
NewOAMAPI = func(aws.Config) models.OAMAPIProvider { return nil }
|
||||
|
||||
var logApi mocks.LogsAPI
|
||||
NewLogsAPI = func(sess *session.Session) models.CloudWatchLogsAPIProvider {
|
||||
NewLogsAPI = func(aws.Config) models.CloudWatchLogsAPIProvider {
|
||||
return &logApi
|
||||
}
|
||||
ec2Mock := &mocks.EC2Mock{}
|
||||
ec2Mock.On("DescribeRegionsWithContext", mock.Anything, mock.Anything).Return(&ec2.DescribeRegionsOutput{}, nil)
|
||||
NewEC2Client = func(provider client.ConfigProvider) models.EC2APIProvider {
|
||||
ec2Mock.On("DescribeRegions", mock.Anything, mock.Anything).Return(&ec2.DescribeRegionsOutput{}, nil)
|
||||
NewEC2API = func(aws.Config) models.EC2APIProvider {
|
||||
return ec2Mock
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
NewOAMAPI = origNewOAMAPI
|
||||
NewMetricsAPI = origNewMetricsAPI
|
||||
NewCWClient = origNewCWClient
|
||||
NewLogsAPI = origNewLogsAPI
|
||||
NewEC2Client = origNewEC2Client
|
||||
NewEC2API = origNewEC2API
|
||||
})
|
||||
|
||||
var api mocks.FakeMetricsAPI
|
||||
NewMetricsAPI = func(sess *session.Session) models.CloudWatchMetricsAPIProvider {
|
||||
NewCWClient = func(aws.Config) models.CWClient {
|
||||
return &api
|
||||
}
|
||||
|
||||
t.Run("Should handle dimension value request and return values from the api", func(t *testing.T) {
|
||||
im := testInstanceManager(100)
|
||||
api = mocks.FakeMetricsAPI{Metrics: []*cloudwatch.Metric{
|
||||
{MetricName: aws.String("Test_MetricName1"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value1")}, {Name: aws.String("Test_DimensionName2"), Value: aws.String("Value2")}}},
|
||||
{MetricName: aws.String("Test_MetricName2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value3")}}},
|
||||
{MetricName: aws.String("Test_MetricName3"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2"), Value: aws.String("Value1")}}},
|
||||
{MetricName: aws.String("Test_MetricName10"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value2")}, {Name: aws.String("Test_DimensionName5")}}},
|
||||
{MetricName: aws.String("Test_MetricName4"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2"), Value: aws.String("Value3")}}},
|
||||
{MetricName: aws.String("Test_MetricName5"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value4")}}},
|
||||
{MetricName: aws.String("Test_MetricName6"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value6")}}},
|
||||
{MetricName: aws.String("Test_MetricName7"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value7")}}},
|
||||
{MetricName: aws.String("Test_MetricName8"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value1")}}},
|
||||
{MetricName: aws.String("Test_MetricName9"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value2")}}},
|
||||
api = mocks.FakeMetricsAPI{Metrics: []cloudwatchtypes.Metric{
|
||||
{MetricName: aws.String("Test_MetricName1"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value1")}, {Name: aws.String("Test_DimensionName2"), Value: aws.String("Value2")}}},
|
||||
{MetricName: aws.String("Test_MetricName2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value3")}}},
|
||||
{MetricName: aws.String("Test_MetricName3"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName2"), Value: aws.String("Value1")}}},
|
||||
{MetricName: aws.String("Test_MetricName10"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value2")}, {Name: aws.String("Test_DimensionName5")}}},
|
||||
{MetricName: aws.String("Test_MetricName4"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName2"), Value: aws.String("Value3")}}},
|
||||
{MetricName: aws.String("Test_MetricName5"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value4")}}},
|
||||
{MetricName: aws.String("Test_MetricName6"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value6")}}},
|
||||
{MetricName: aws.String("Test_MetricName7"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value7")}}},
|
||||
{MetricName: aws.String("Test_MetricName8"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value1")}}},
|
||||
{MetricName: aws.String("Test_MetricName9"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value2")}}},
|
||||
}, MetricsPerPage: 100}
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.GrafanaSettings.ListMetricsPageLimit = 100
|
||||
})
|
||||
|
||||
req := &backend.CallResourceRequest{
|
||||
Method: "GET",
|
||||
@@ -78,7 +75,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
PluginID: "cloudwatch",
|
||||
},
|
||||
}
|
||||
err := executor.CallResource(context.Background(), req, sender)
|
||||
err := ds.CallResource(context.Background(), req, sender)
|
||||
|
||||
require.NoError(t, err)
|
||||
sent := sender.Response
|
||||
@@ -91,20 +88,21 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Should handle dimension key filter query and return keys from the api", func(t *testing.T) {
|
||||
im := testInstanceManager(3)
|
||||
api = mocks.FakeMetricsAPI{Metrics: []*cloudwatch.Metric{
|
||||
{MetricName: aws.String("Test_MetricName1"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}, {Name: aws.String("Test_DimensionName2")}}},
|
||||
{MetricName: aws.String("Test_MetricName2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
{MetricName: aws.String("Test_MetricName3"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2")}}},
|
||||
{MetricName: aws.String("Test_MetricName10"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}, {Name: aws.String("Test_DimensionName5")}}},
|
||||
{MetricName: aws.String("Test_MetricName4"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2")}}},
|
||||
{MetricName: aws.String("Test_MetricName5"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
{MetricName: aws.String("Test_MetricName6"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
{MetricName: aws.String("Test_MetricName7"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}}},
|
||||
{MetricName: aws.String("Test_MetricName8"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}}},
|
||||
{MetricName: aws.String("Test_MetricName9"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
api = mocks.FakeMetricsAPI{Metrics: []cloudwatchtypes.Metric{
|
||||
{MetricName: aws.String("Test_MetricName1"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}, {Name: aws.String("Test_DimensionName2")}}},
|
||||
{MetricName: aws.String("Test_MetricName2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
{MetricName: aws.String("Test_MetricName3"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName2")}}},
|
||||
{MetricName: aws.String("Test_MetricName10"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4")}, {Name: aws.String("Test_DimensionName5")}}},
|
||||
{MetricName: aws.String("Test_MetricName4"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName2")}}},
|
||||
{MetricName: aws.String("Test_MetricName5"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
{MetricName: aws.String("Test_MetricName6"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
{MetricName: aws.String("Test_MetricName7"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4")}}},
|
||||
{MetricName: aws.String("Test_MetricName8"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4")}}},
|
||||
{MetricName: aws.String("Test_MetricName9"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
}, MetricsPerPage: 2}
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.GrafanaSettings.ListMetricsPageLimit = 3
|
||||
})
|
||||
|
||||
req := &backend.CallResourceRequest{
|
||||
Method: "GET",
|
||||
@@ -114,7 +112,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
PluginID: "cloudwatch",
|
||||
},
|
||||
}
|
||||
err := executor.CallResource(context.Background(), req, sender)
|
||||
err := ds.CallResource(context.Background(), req, sender)
|
||||
|
||||
require.NoError(t, err)
|
||||
sent := sender.Response
|
||||
@@ -127,9 +125,10 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Should handle standard dimension key query and return hard coded keys", func(t *testing.T) {
|
||||
im := defaultTestInstanceManager()
|
||||
api = mocks.FakeMetricsAPI{}
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
|
||||
})
|
||||
|
||||
req := &backend.CallResourceRequest{
|
||||
Method: "GET",
|
||||
@@ -139,7 +138,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
PluginID: "cloudwatch",
|
||||
},
|
||||
}
|
||||
err := executor.CallResource(context.Background(), req, sender)
|
||||
err := ds.CallResource(context.Background(), req, sender)
|
||||
|
||||
require.NoError(t, err)
|
||||
sent := sender.Response
|
||||
@@ -152,9 +151,8 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Should handle custom namespace dimension key query and return hard coded keys", func(t *testing.T) {
|
||||
im := defaultTestInstanceManager()
|
||||
api = mocks.FakeMetricsAPI{}
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource()
|
||||
|
||||
req := &backend.CallResourceRequest{
|
||||
Method: "GET",
|
||||
@@ -164,7 +162,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
PluginID: "cloudwatch",
|
||||
},
|
||||
}
|
||||
err := executor.CallResource(context.Background(), req, sender)
|
||||
err := ds.CallResource(context.Background(), req, sender)
|
||||
|
||||
require.NoError(t, err)
|
||||
sent := sender.Response
|
||||
@@ -177,20 +175,21 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Should handle custom namespace metrics query and return metrics from api", func(t *testing.T) {
|
||||
im := testInstanceManager(3)
|
||||
api = mocks.FakeMetricsAPI{Metrics: []*cloudwatch.Metric{
|
||||
{MetricName: aws.String("Test_MetricName1"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}, {Name: aws.String("Test_DimensionName2")}}},
|
||||
{MetricName: aws.String("Test_MetricName2"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
{MetricName: aws.String("Test_MetricName3"), Namespace: aws.String("AWS/ECS"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2")}}},
|
||||
{MetricName: aws.String("Test_MetricName10"), Namespace: aws.String("AWS/ECS"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}, {Name: aws.String("Test_DimensionName5")}}},
|
||||
{MetricName: aws.String("Test_MetricName4"), Namespace: aws.String("AWS/ECS"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2")}}},
|
||||
{MetricName: aws.String("Test_MetricName5"), Namespace: aws.String("AWS/Redshift"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
{MetricName: aws.String("Test_MetricName6"), Namespace: aws.String("AWS/Redshift"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
{MetricName: aws.String("Test_MetricName7"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}}},
|
||||
{MetricName: aws.String("Test_MetricName8"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}}},
|
||||
{MetricName: aws.String("Test_MetricName9"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
api = mocks.FakeMetricsAPI{Metrics: []cloudwatchtypes.Metric{
|
||||
{MetricName: aws.String("Test_MetricName1"), Namespace: aws.String("AWS/EC2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}, {Name: aws.String("Test_DimensionName2")}}},
|
||||
{MetricName: aws.String("Test_MetricName2"), Namespace: aws.String("AWS/EC2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
{MetricName: aws.String("Test_MetricName3"), Namespace: aws.String("AWS/ECS"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName2")}}},
|
||||
{MetricName: aws.String("Test_MetricName10"), Namespace: aws.String("AWS/ECS"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4")}, {Name: aws.String("Test_DimensionName5")}}},
|
||||
{MetricName: aws.String("Test_MetricName4"), Namespace: aws.String("AWS/ECS"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName2")}}},
|
||||
{MetricName: aws.String("Test_MetricName5"), Namespace: aws.String("AWS/Redshift"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
{MetricName: aws.String("Test_MetricName6"), Namespace: aws.String("AWS/Redshift"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
{MetricName: aws.String("Test_MetricName7"), Namespace: aws.String("AWS/EC2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4")}}},
|
||||
{MetricName: aws.String("Test_MetricName8"), Namespace: aws.String("AWS/EC2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName4")}}},
|
||||
{MetricName: aws.String("Test_MetricName9"), Namespace: aws.String("AWS/EC2"), Dimensions: []cloudwatchtypes.Dimension{{Name: aws.String("Test_DimensionName1")}}},
|
||||
}, MetricsPerPage: 2}
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.GrafanaSettings.ListMetricsPageLimit = 3
|
||||
})
|
||||
|
||||
req := &backend.CallResourceRequest{
|
||||
Method: "GET",
|
||||
@@ -200,7 +199,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
PluginID: "cloudwatch",
|
||||
},
|
||||
}
|
||||
err := executor.CallResource(context.Background(), req, sender)
|
||||
err := ds.CallResource(context.Background(), req, sender)
|
||||
|
||||
require.NoError(t, err)
|
||||
sent := sender.Response
|
||||
@@ -213,21 +212,20 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Should handle log group fields request", func(t *testing.T) {
|
||||
im := defaultTestInstanceManager()
|
||||
logApi = mocks.LogsAPI{}
|
||||
logApi.On("GetLogGroupFieldsWithContext", mock.Anything).Return(&cloudwatchlogs.GetLogGroupFieldsOutput{
|
||||
LogGroupFields: []*cloudwatchlogs.LogGroupField{
|
||||
logApi.On("GetLogGroupFields", mock.Anything).Return(&cloudwatchlogs.GetLogGroupFieldsOutput{
|
||||
LogGroupFields: []cloudwatchlogstypes.LogGroupField{
|
||||
{
|
||||
Name: aws.String("field1"),
|
||||
Percent: aws.Int64(50),
|
||||
Percent: 50,
|
||||
},
|
||||
{
|
||||
Name: aws.String("field2"),
|
||||
Percent: aws.Int64(50),
|
||||
Percent: 50,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource()
|
||||
|
||||
req := &backend.CallResourceRequest{
|
||||
Method: "GET",
|
||||
@@ -237,7 +235,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
PluginID: "cloudwatch",
|
||||
},
|
||||
}
|
||||
err := executor.CallResource(context.Background(), req, sender)
|
||||
err := ds.CallResource(context.Background(), req, sender)
|
||||
|
||||
require.NoError(t, err)
|
||||
sent := sender.Response
|
||||
@@ -248,8 +246,9 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Should handle region requests and return regions from the api", func(t *testing.T) {
|
||||
im := defaultTestInstanceManager()
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.Region = "us-east-2"
|
||||
})
|
||||
req := &backend.CallResourceRequest{
|
||||
Method: "GET",
|
||||
Path: `/regions`,
|
||||
@@ -258,7 +257,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
PluginID: "cloudwatch",
|
||||
},
|
||||
}
|
||||
err := executor.CallResource(context.Background(), req, sender)
|
||||
err := ds.CallResource(context.Background(), req, sender)
|
||||
require.NoError(t, err)
|
||||
sent := sender.Response
|
||||
require.NotNil(t, sent)
|
||||
@@ -268,14 +267,11 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Should error for any request when a default region is not selected", func(t *testing.T) {
|
||||
imWithoutDefaultRegion := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{
|
||||
AWSDatasourceSettings: awsds.AWSDatasourceSettings{},
|
||||
GrafanaSettings: awsds.AuthSettings{ListMetricsPageLimit: 1000},
|
||||
}}, nil
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.GrafanaSettings.ListMetricsPageLimit = 1000
|
||||
ds.Settings.Region = ""
|
||||
})
|
||||
|
||||
executor := newExecutor(imWithoutDefaultRegion, log.NewNullLogger())
|
||||
req := &backend.CallResourceRequest{
|
||||
Method: "GET",
|
||||
Path: `/regions`,
|
||||
@@ -284,7 +280,7 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
|
||||
PluginID: "cloudwatch",
|
||||
},
|
||||
}
|
||||
err := executor.CallResource(context.Background(), req, sender)
|
||||
err := ds.CallResource(context.Background(), req, sender)
|
||||
require.NoError(t, err)
|
||||
sent := sender.Response
|
||||
require.NotNil(t, sent)
|
||||
|
||||
@@ -6,18 +6,15 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
awsclient "github.com/aws/aws-sdk-go/aws/client"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
|
||||
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsauth"
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
|
||||
@@ -34,7 +31,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
name string
|
||||
settings backend.DataSourceInstanceSettings
|
||||
settingCtx context.Context
|
||||
expectedDS DataSource
|
||||
expectedDS *DataSource
|
||||
Err require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
@@ -62,7 +59,7 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
awsds.ListMetricsPageLimitKeyName: "50",
|
||||
proxy.PluginSecureSocksProxyEnabled: "true",
|
||||
})),
|
||||
expectedDS: DataSource{
|
||||
expectedDS: &DataSource{
|
||||
Settings: models.CloudWatchSettings{
|
||||
AWSDatasourceSettings: awsds.AWSDatasourceSettings{
|
||||
Profile: "foo",
|
||||
@@ -91,11 +88,11 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := NewInstanceSettings(httpclient.NewProvider())
|
||||
model, err := f(tt.settingCtx, tt.settings)
|
||||
instance, err := NewDatasource(tt.settingCtx, tt.settings)
|
||||
ds := instance.(*DataSource)
|
||||
tt.Err(t, err)
|
||||
assert.Equal(t, tt.expectedDS.Settings.GrafanaSettings, model.(DataSource).Settings.GrafanaSettings)
|
||||
datasourceComparer := cmp.Comparer(func(d1 DataSource, d2 DataSource) bool {
|
||||
assert.Equal(t, tt.expectedDS.Settings.GrafanaSettings, ds.Settings.GrafanaSettings)
|
||||
datasourceComparer := cmp.Comparer(func(d1 *DataSource, d2 *DataSource) bool {
|
||||
return d1.Settings.Profile == d2.Settings.Profile &&
|
||||
d1.Settings.Region == d2.Settings.Region &&
|
||||
d1.Settings.AuthType == d2.Settings.AuthType &&
|
||||
@@ -106,40 +103,39 @@ func TestNewInstanceSettings(t *testing.T) {
|
||||
d1.Settings.AccessKey == d2.Settings.AccessKey &&
|
||||
d1.Settings.SecretKey == d2.Settings.SecretKey
|
||||
})
|
||||
if !cmp.Equal(model.(DataSource), tt.expectedDS, datasourceComparer) {
|
||||
t.Errorf("Unexpected result. Expecting\n%v \nGot:\n%v", model, tt.expectedDS)
|
||||
if !cmp.Equal(instance.(*DataSource), tt.expectedDS, datasourceComparer) {
|
||||
t.Errorf("Unexpected result. Expecting\n%v \nGot:\n%v", instance, tt.expectedDS)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CheckHealth(t *testing.T) {
|
||||
origNewMetricsAPI := NewMetricsAPI
|
||||
origNewCWClient := NewCWClient
|
||||
origNewCWLogsClient := NewCWLogsClient
|
||||
origNewLogsAPI := NewLogsAPI
|
||||
|
||||
t.Cleanup(func() {
|
||||
NewMetricsAPI = origNewMetricsAPI
|
||||
NewCWClient = origNewCWClient
|
||||
NewCWLogsClient = origNewCWLogsClient
|
||||
NewLogsAPI = origNewLogsAPI
|
||||
})
|
||||
|
||||
var client fakeCheckHealthClient
|
||||
NewMetricsAPI = func(sess *session.Session) models.CloudWatchMetricsAPIProvider {
|
||||
NewCWClient = func(aws.Config) models.CWClient {
|
||||
return client
|
||||
}
|
||||
NewLogsAPI = func(sess *session.Session) models.CloudWatchLogsAPIProvider {
|
||||
NewLogsAPI = func(aws.Config) models.CloudWatchLogsAPIProvider {
|
||||
return client
|
||||
}
|
||||
im := defaultTestInstanceManager()
|
||||
|
||||
t.Run("successfully query metrics and logs", func(t *testing.T) {
|
||||
client = fakeCheckHealthClient{}
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
|
||||
resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.Region = "us-east-1"
|
||||
})
|
||||
resp, err := ds.CheckHealth(context.Background(), &backend.CheckHealthRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &backend.CheckHealthResult{
|
||||
@@ -149,14 +145,15 @@ func Test_CheckHealth(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("successfully queries metrics, fails during logs query", func(t *testing.T) {
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.Region = "us-east-1"
|
||||
})
|
||||
client = fakeCheckHealthClient{
|
||||
describeLogGroups: func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
|
||||
describeLogGroupsFunction: func(context.Context, *cloudwatchlogs.DescribeLogGroupsInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
|
||||
return nil, fmt.Errorf("some logs query error")
|
||||
}}
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
|
||||
resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{
|
||||
},
|
||||
}
|
||||
resp, err := ds.CheckHealth(context.Background(), &backend.CheckHealthRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
})
|
||||
|
||||
@@ -168,14 +165,15 @@ func Test_CheckHealth(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("successfully queries logs, fails during metrics query", func(t *testing.T) {
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.Region = "us-east-1"
|
||||
ds.Settings.GrafanaSettings.ListMetricsPageLimit = 1
|
||||
})
|
||||
client = fakeCheckHealthClient{
|
||||
listMetricsPages: func(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error {
|
||||
return fmt.Errorf("some list metrics error")
|
||||
listMetricsFunction: func(context.Context, *cloudwatch.ListMetricsInput, ...func(*cloudwatch.Options)) (*cloudwatch.ListMetricsOutput, error) {
|
||||
return nil, fmt.Errorf("some list metrics error")
|
||||
}}
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
|
||||
resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{
|
||||
resp, err := ds.CheckHealth(context.Background(), &backend.CheckHealthRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
})
|
||||
|
||||
@@ -188,30 +186,25 @@ func Test_CheckHealth(t *testing.T) {
|
||||
|
||||
t.Run("fail to get clients", func(t *testing.T) {
|
||||
client = fakeCheckHealthClient{}
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{
|
||||
Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "us-east-1"}},
|
||||
sessions: &fakeSessionCache{getSessionWithAuthSettings: func(c awsds.GetSessionConfig, a awsds.AuthSettings) (*session.Session, error) {
|
||||
return nil, fmt.Errorf("some sessions error")
|
||||
}},
|
||||
}, nil
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.AWSConfigProvider = awsauth.NewFakeConfigProvider(true)
|
||||
ds.Settings.Region = "us-east-1"
|
||||
})
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
|
||||
resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{
|
||||
resp, err := ds.CheckHealth(context.Background(), &backend.CheckHealthRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusError,
|
||||
Message: "1. CloudWatch metrics query failed: some sessions error\n2. CloudWatch logs query failed: some sessions error",
|
||||
Message: "1. CloudWatch metrics query failed: LoadDefaultConfig failed\n2. CloudWatch logs query failed: LoadDefaultConfig failed",
|
||||
}, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewSession_passes_authSettings(t *testing.T) {
|
||||
func TestGetAWSConfig_passes_authSettings(t *testing.T) {
|
||||
// TODO: update this for the new auth structure, or remove it
|
||||
t.Skip()
|
||||
ctxDuration := 15 * time.Minute
|
||||
expectedSettings := awsds.AuthSettings{
|
||||
AllowedAuthProviders: []string{"foo", "bar", "baz"},
|
||||
@@ -221,56 +214,40 @@ func TestNewSession_passes_authSettings(t *testing.T) {
|
||||
ListMetricsPageLimit: 50,
|
||||
SecureSocksDSProxyEnabled: true,
|
||||
}
|
||||
im := datasource.NewInstanceManager((func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{
|
||||
Settings: models.CloudWatchSettings{
|
||||
AWSDatasourceSettings: awsds.AWSDatasourceSettings{
|
||||
Region: "us-east-1",
|
||||
},
|
||||
GrafanaSettings: expectedSettings,
|
||||
},
|
||||
sessions: &fakeSessionCache{getSessionWithAuthSettings: func(c awsds.GetSessionConfig, a awsds.AuthSettings) (*session.Session, error) {
|
||||
assert.Equal(t, expectedSettings, a)
|
||||
return &session.Session{
|
||||
Config: &aws.Config{},
|
||||
}, nil
|
||||
}},
|
||||
}, nil
|
||||
}))
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.Region = "us-east-1"
|
||||
ds.Settings.GrafanaSettings = expectedSettings
|
||||
})
|
||||
|
||||
_, err := executor.newSessionFromContext(context.Background(),
|
||||
backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, "us-east-1")
|
||||
_, err := ds.getAWSConfig(context.Background(), "us-east-1")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestQuery_ResourceRequest_DescribeLogGroups_with_CrossAccountQuerying(t *testing.T) {
|
||||
sender := &mockedCallResourceResponseSenderForOauth{}
|
||||
origNewMetricsAPI := NewMetricsAPI
|
||||
origNewMetricsAPI := NewCWClient
|
||||
origNewOAMAPI := NewOAMAPI
|
||||
origNewLogsAPI := NewLogsAPI
|
||||
origNewEC2Client := NewEC2Client
|
||||
NewMetricsAPI = func(sess *session.Session) models.CloudWatchMetricsAPIProvider { return nil }
|
||||
NewOAMAPI = func(sess *session.Session) models.OAMAPIProvider { return nil }
|
||||
NewEC2Client = func(provider awsclient.ConfigProvider) models.EC2APIProvider { return nil }
|
||||
origNewEC2API := NewEC2API
|
||||
NewCWClient = func(aws.Config) models.CWClient { return nil }
|
||||
NewOAMAPI = func(aws.Config) models.OAMAPIProvider { return nil }
|
||||
NewEC2API = func(aws.Config) models.EC2APIProvider { return nil }
|
||||
t.Cleanup(func() {
|
||||
NewOAMAPI = origNewOAMAPI
|
||||
NewMetricsAPI = origNewMetricsAPI
|
||||
NewCWClient = origNewMetricsAPI
|
||||
NewLogsAPI = origNewLogsAPI
|
||||
NewEC2Client = origNewEC2Client
|
||||
NewEC2API = origNewEC2API
|
||||
})
|
||||
|
||||
var logsApi mocks.LogsAPI
|
||||
NewLogsAPI = func(sess *session.Session) models.CloudWatchLogsAPIProvider {
|
||||
NewLogsAPI = func(aws.Config) models.CloudWatchLogsAPIProvider {
|
||||
return &logsApi
|
||||
}
|
||||
|
||||
im := defaultTestInstanceManager()
|
||||
|
||||
t.Run("maps log group api response to resource response of log-groups", func(t *testing.T) {
|
||||
logsApi = mocks.LogsAPI{}
|
||||
logsApi.On("DescribeLogGroupsWithContext", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{
|
||||
LogGroups: []*cloudwatchlogs.LogGroup{
|
||||
logsApi.On("DescribeLogGroups", mock.Anything).Return(&cloudwatchlogs.DescribeLogGroupsOutput{
|
||||
LogGroups: []cloudwatchlogstypes.LogGroup{
|
||||
{Arn: aws.String("arn:aws:logs:us-east-1:111:log-group:group_a"), LogGroupName: aws.String("group_a")},
|
||||
},
|
||||
}, nil)
|
||||
@@ -283,8 +260,9 @@ func TestQuery_ResourceRequest_DescribeLogGroups_with_CrossAccountQuerying(t *te
|
||||
},
|
||||
}
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
err := executor.CallResource(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), req, sender)
|
||||
ds := newTestDatasource()
|
||||
|
||||
err := ds.CallResource(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), req, sender)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, `[
|
||||
@@ -297,11 +275,11 @@ func TestQuery_ResourceRequest_DescribeLogGroups_with_CrossAccountQuerying(t *te
|
||||
}
|
||||
]`, string(sender.Response.Body))
|
||||
|
||||
logsApi.AssertCalled(t, "DescribeLogGroupsWithContext",
|
||||
logsApi.AssertCalled(t, "DescribeLogGroups",
|
||||
&cloudwatchlogs.DescribeLogGroupsInput{
|
||||
AccountIdentifiers: []*string{utils.Pointer("some-account-id")},
|
||||
AccountIdentifiers: []string{"some-account-id"},
|
||||
IncludeLinkedAccounts: utils.Pointer(true),
|
||||
Limit: utils.Pointer(int64(50)),
|
||||
Limit: aws.Int32(50),
|
||||
LogGroupNamePrefix: utils.Pointer("some-pattern"),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
package constants
|
||||
|
||||
import "github.com/grafana/grafana-aws-sdk/pkg/cloudWatchConsts"
|
||||
|
||||
// NamespaceMetricsMap is a map of Cloudwatch namespaces to their metrics
|
||||
// Deprecated: use cloudWatchConsts.NamespaceMetricsMap from grafana-aws-sdk instead
|
||||
var NamespaceMetricsMap = cloudWatchConsts.NamespaceMetricsMap
|
||||
|
||||
// NamespaceDimensionKeysMap is a map of CloudWatch namespaces to their dimension keys
|
||||
// Deprecated: use cloudWatchConsts.NamespaceDimensionKeysMap from grafana-aws-sdk instead
|
||||
var NamespaceDimensionKeysMap = cloudWatchConsts.NamespaceDimensionKeysMap
|
||||
|
||||
type RegionsSet map[string]struct{}
|
||||
|
||||
func Regions() RegionsSet {
|
||||
return RegionsSet{
|
||||
"af-south-1": {},
|
||||
"ap-east-1": {},
|
||||
"ap-east-2": {},
|
||||
"ap-northeast-1": {},
|
||||
"ap-northeast-2": {},
|
||||
"ap-northeast-3": {},
|
||||
@@ -25,7 +16,10 @@ func Regions() RegionsSet {
|
||||
"ap-southeast-2": {},
|
||||
"ap-southeast-3": {},
|
||||
"ap-southeast-4": {},
|
||||
"ap-southeast-5": {},
|
||||
"ap-southeast-7": {},
|
||||
"ca-central-1": {},
|
||||
"ca-west-1": {},
|
||||
"cn-north-1": {},
|
||||
"cn-northwest-1": {},
|
||||
"eu-central-1": {},
|
||||
@@ -39,6 +33,7 @@ func Regions() RegionsSet {
|
||||
"il-central-1": {},
|
||||
"me-central-1": {},
|
||||
"me-south-1": {},
|
||||
"mx-central-1": {},
|
||||
"sa-east-1": {},
|
||||
"us-east-1": {},
|
||||
"us-east-2": {},
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package routes
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -20,11 +17,19 @@ import (
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
|
||||
)
|
||||
|
||||
var logger = log.NewNullLogger()
|
||||
|
||||
func Test_DimensionKeys_Route(t *testing.T) {
|
||||
origNewListMetricsService := services.NewListMetricsService
|
||||
t.Cleanup(func() {
|
||||
services.NewListMetricsService = origNewListMetricsService
|
||||
})
|
||||
|
||||
var mockListMetricsService mocks.ListMetricsServiceMock
|
||||
services.NewListMetricsService = func(models.MetricsClientProvider) models.ListMetricsProvider {
|
||||
return &mockListMetricsService
|
||||
}
|
||||
|
||||
t.Run("calls FilterDimensionKeysRequest when a StandardDimensionKeysRequest is passed", func(t *testing.T) {
|
||||
mockListMetricsService := mocks.ListMetricsServiceMock{}
|
||||
mockListMetricsService = mocks.ListMetricsServiceMock{}
|
||||
mockListMetricsService.On("GetDimensionKeysByDimensionFilter", mock.MatchedBy(func(r resources.DimensionKeysRequest) bool {
|
||||
return r.ResourceRequest != nil && *r.ResourceRequest == resources.ResourceRequest{Region: "us-east-2"} &&
|
||||
r.Namespace == "AWS/EC2" &&
|
||||
@@ -33,12 +38,10 @@ func Test_DimensionKeys_Route(t *testing.T) {
|
||||
assert.Contains(t, r.DimensionFilter, &resources.Dimension{Name: "NodeID", Value: "Shared"}) &&
|
||||
assert.Contains(t, r.DimensionFilter, &resources.Dimension{Name: "stage", Value: "QueryCommit"})
|
||||
})).Return([]resources.ResourceResponse[string]{}, nil).Once()
|
||||
newListMetricsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
|
||||
return &mockListMetricsService, nil
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", `/dimension-keys?region=us-east-2&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`, nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, logger, nil))
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.DimensionKeysHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
})
|
||||
|
||||
@@ -56,7 +59,8 @@ func Test_DimensionKeys_Route(t *testing.T) {
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/dimension-keys?region=us-east-2&namespace=AWS/EC2&metricName=CPUUtilization", nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, logger, nil))
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.DimensionKeysHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
res := []resources.Metric{}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &res)
|
||||
@@ -66,14 +70,12 @@ func Test_DimensionKeys_Route(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("return 500 if GetDimensionKeysByDimensionFilter returns an error", func(t *testing.T) {
|
||||
mockListMetricsService := mocks.ListMetricsServiceMock{}
|
||||
mockListMetricsService = mocks.ListMetricsServiceMock{}
|
||||
mockListMetricsService.On("GetDimensionKeysByDimensionFilter", mock.Anything).Return([]resources.ResourceResponse[string]{}, fmt.Errorf("some error"))
|
||||
newListMetricsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
|
||||
return &mockListMetricsService, nil
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", `/dimension-keys?region=us-east-2&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`, nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, logger, nil))
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.DimensionKeysHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
assert.Equal(t, `{"Message":"error in DimensionKeyHandler: some error","Error":"some error","StatusCode":500}`, rr.Body.String())
|
||||
@@ -1,14 +1,13 @@
|
||||
package routes
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
@@ -17,8 +16,18 @@ import (
|
||||
)
|
||||
|
||||
func Test_DimensionValues_Route(t *testing.T) {
|
||||
origNewListMetricsService := services.NewListMetricsService
|
||||
t.Cleanup(func() {
|
||||
services.NewListMetricsService = origNewListMetricsService
|
||||
})
|
||||
|
||||
var mockListMetricsService mocks.ListMetricsServiceMock
|
||||
services.NewListMetricsService = func(models.MetricsClientProvider) models.ListMetricsProvider {
|
||||
return &mockListMetricsService
|
||||
}
|
||||
|
||||
t.Run("Calls GetDimensionValuesByDimensionFilter when a valid request is passed", func(t *testing.T) {
|
||||
mockListMetricsService := mocks.ListMetricsServiceMock{}
|
||||
mockListMetricsService = mocks.ListMetricsServiceMock{}
|
||||
mockListMetricsService.On("GetDimensionValuesByDimensionFilter", mock.MatchedBy(func(r resources.DimensionValuesRequest) bool {
|
||||
return r.ResourceRequest != nil && *r.ResourceRequest == resources.ResourceRequest{Region: "us-east-2"} &&
|
||||
r.Namespace == "AWS/EC2" &&
|
||||
@@ -28,24 +37,20 @@ func Test_DimensionValues_Route(t *testing.T) {
|
||||
assert.Contains(t, r.DimensionFilter, &resources.Dimension{Name: "NodeID", Value: "Shared"}) &&
|
||||
assert.Contains(t, r.DimensionFilter, &resources.Dimension{Name: "stage", Value: "QueryCommit"})
|
||||
})).Return([]resources.ResourceResponse[string]{}, nil).Once()
|
||||
newListMetricsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
|
||||
return &mockListMetricsService, nil
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", `/dimension-values?region=us-east-2&dimensionKey=instanceId&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`, nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionValuesHandler, logger, nil))
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.DimensionValuesHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
})
|
||||
|
||||
t.Run("returns 500 if GetDimensionValuesByDimensionFilter returns an error", func(t *testing.T) {
|
||||
mockListMetricsService := mocks.ListMetricsServiceMock{}
|
||||
mockListMetricsService = mocks.ListMetricsServiceMock{}
|
||||
mockListMetricsService.On("GetDimensionValuesByDimensionFilter", mock.Anything).Return([]resources.ResourceResponse[string]{}, fmt.Errorf("some error"))
|
||||
newListMetricsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
|
||||
return &mockListMetricsService, nil
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", `/dimension-values?region=us-east-2&dimensionKey=instanceId&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`, nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionValuesHandler, logger, nil))
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.DimensionValuesHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
assert.Equal(t, `{"Message":"error in DimensionValuesHandler: some error","Error":"some error","StatusCode":500}`, rr.Body.String())
|
||||
40
pkg/tsdb/cloudwatch/external_id_test.go
Normal file
40
pkg/tsdb/cloudwatch/external_id_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_external_id_route(t *testing.T) {
|
||||
t.Run("successfully returns an external id from the instance", func(t *testing.T) {
|
||||
t.Setenv("AWS_AUTH_EXTERNAL_ID", "mock-external-id")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.GrafanaSettings.ExternalID = "mock-external-id"
|
||||
})
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.ExternalIdHandler))
|
||||
req := httptest.NewRequest("GET", "/external-id", nil)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.JSONEq(t, `{"externalId":"mock-external-id"}`, rr.Body.String())
|
||||
})
|
||||
|
||||
t.Run("returns an empty string if there is no external id", func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.ExternalIdHandler))
|
||||
req := httptest.NewRequest("GET", "/external-id", nil)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.JSONEq(t, `{"externalId":""}`, rr.Body.String())
|
||||
})
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
@@ -26,10 +27,10 @@ func shouldSkipFetchingWildcards(ctx context.Context, q *models.CloudWatchQuery)
|
||||
}
|
||||
|
||||
// getDimensionValues gets the actual dimension values for dimensions with a wildcard
|
||||
func (e *cloudWatchExecutor) getDimensionValuesForWildcards(
|
||||
func (ds *DataSource) getDimensionValuesForWildcards(
|
||||
ctx context.Context,
|
||||
region string,
|
||||
client models.CloudWatchMetricsAPIProvider,
|
||||
client models.CWClient,
|
||||
origQueries []*models.CloudWatchQuery,
|
||||
tagValueCache *cache.Cache,
|
||||
listMetricsPageLimit int,
|
||||
@@ -57,12 +58,12 @@ func (e *cloudWatchExecutor) getDimensionValuesForWildcards(
|
||||
cacheKey := fmt.Sprintf("%s-%s-%s-%s-%s", region, accountID, query.Namespace, query.MetricName, dimensionKey)
|
||||
cachedDimensions, found := tagValueCache.Get(cacheKey)
|
||||
if found {
|
||||
e.logger.FromContext(ctx).Debug("Fetching dimension values from cache")
|
||||
ds.logger.FromContext(ctx).Debug("Fetching dimension values from cache")
|
||||
query.Dimensions[dimensionKey] = cachedDimensions.([]string)
|
||||
continue
|
||||
}
|
||||
|
||||
e.logger.FromContext(ctx).Debug("Cache miss, fetching dimension values from AWS")
|
||||
ds.logger.FromContext(ctx).Debug("Cache miss, fetching dimension values from AWS")
|
||||
request := resources.DimensionValuesRequest{
|
||||
ResourceRequest: &resources.ResourceRequest{
|
||||
Region: region,
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
@@ -14,10 +14,10 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func noSkip(ctx context.Context, q *models.CloudWatchQuery) bool { return false }
|
||||
func noSkip(context.Context, *models.CloudWatchQuery) bool { return false }
|
||||
|
||||
func TestGetDimensionValuesForWildcards(t *testing.T) {
|
||||
executor := &cloudWatchExecutor{im: defaultTestInstanceManager(), logger: log.NewNullLogger()}
|
||||
ds := newTestDatasource()
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("Tag value cache", func(t *testing.T) {
|
||||
@@ -29,17 +29,17 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
|
||||
query.Dimensions = map[string][]string{"Test_DimensionName": {"*"}}
|
||||
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||
query.MatchExact = false
|
||||
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{
|
||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName"), Value: utils.Pointer("Value")}}},
|
||||
api := &mocks.MetricsAPI{Metrics: []cloudwatchtypes.Metric{
|
||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName"), Value: utils.Pointer("Value")}}},
|
||||
}}
|
||||
api.On("ListMetricsPagesWithContext").Return(nil)
|
||||
_, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
|
||||
api.On("ListMetrics").Return(nil)
|
||||
_, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
|
||||
assert.Nil(t, err)
|
||||
// make sure the original query wasn't altered
|
||||
assert.Equal(t, map[string][]string{"Test_DimensionName": {"*"}}, query.Dimensions)
|
||||
|
||||
//setting the api to nil confirms that it's using the cached value
|
||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
|
||||
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, queries, 1)
|
||||
assert.Equal(t, map[string][]string{"Test_DimensionName": {"Value"}}, queries[0].Dimensions)
|
||||
@@ -52,20 +52,20 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
|
||||
query.Dimensions = map[string][]string{"Test_DimensionName2": {"*"}}
|
||||
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||
query.MatchExact = false
|
||||
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{}}
|
||||
api.On("ListMetricsPagesWithContext").Return(nil)
|
||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
|
||||
api := &mocks.MetricsAPI{Metrics: []cloudwatchtypes.Metric{}}
|
||||
api.On("ListMetrics").Return(nil)
|
||||
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, queries, 1)
|
||||
// assert that the values was set to an empty array
|
||||
assert.Equal(t, map[string][]string{"Test_DimensionName2": {}}, queries[0].Dimensions)
|
||||
|
||||
// Confirm that it calls the api again if the last call did not return any values
|
||||
api.Metrics = []*cloudwatch.Metric{
|
||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value")}}},
|
||||
api.Metrics = []cloudwatchtypes.Metric{
|
||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value")}}},
|
||||
}
|
||||
api.On("ListMetricsPagesWithContext").Return(nil)
|
||||
queries, err = executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
|
||||
api.On("ListMetrics").Return(nil)
|
||||
queries, err = ds.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, queries, 1)
|
||||
assert.Equal(t, map[string][]string{"Test_DimensionName2": {"Value"}}, queries[0].Dimensions)
|
||||
@@ -81,7 +81,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
|
||||
query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}}
|
||||
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||
|
||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
|
||||
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, queries, 1)
|
||||
assert.Equal(t, []string{"*"}, queries[0].Dimensions["Test_DimensionName1"])
|
||||
@@ -93,7 +93,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
|
||||
query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}}
|
||||
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||
|
||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
|
||||
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, queries, 1)
|
||||
assert.Equal(t, []string{"*"}, queries[0].Dimensions["Test_DimensionName1"])
|
||||
@@ -107,7 +107,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
|
||||
query.Dimensions = map[string][]string{"Test_DimensionName1": {"Value1"}}
|
||||
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||
query.MatchExact = false
|
||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, shouldSkipFetchingWildcards)
|
||||
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, shouldSkipFetchingWildcards)
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, queries, 1)
|
||||
assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"], 1)
|
||||
@@ -119,7 +119,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
|
||||
query.MetricName = "Test_MetricName1"
|
||||
query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}}
|
||||
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, shouldSkipFetchingWildcards)
|
||||
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, shouldSkipFetchingWildcards)
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, queries, 1)
|
||||
assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"])
|
||||
@@ -132,14 +132,14 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
|
||||
query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}}
|
||||
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||
query.MatchExact = false
|
||||
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{
|
||||
{MetricName: utils.Pointer("Test_MetricName1"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value2")}}},
|
||||
{MetricName: utils.Pointer("Test_MetricName2"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value3")}}},
|
||||
{MetricName: utils.Pointer("Test_MetricName3"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value4")}}},
|
||||
{MetricName: utils.Pointer("Test_MetricName4"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value2")}}},
|
||||
api := &mocks.MetricsAPI{Metrics: []cloudwatchtypes.Metric{
|
||||
{MetricName: utils.Pointer("Test_MetricName1"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value2")}}},
|
||||
{MetricName: utils.Pointer("Test_MetricName2"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value3")}}},
|
||||
{MetricName: utils.Pointer("Test_MetricName3"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value4")}}},
|
||||
{MetricName: utils.Pointer("Test_MetricName4"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value2")}}},
|
||||
}}
|
||||
api.On("ListMetricsPagesWithContext").Return(nil)
|
||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, shouldSkipFetchingWildcards)
|
||||
api.On("ListMetrics").Return(nil)
|
||||
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, shouldSkipFetchingWildcards)
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, queries, 1)
|
||||
assert.Equal(t, map[string][]string{"Test_DimensionName1": {"Value1", "Value2", "Value3", "Value4"}}, queries[0].Dimensions)
|
||||
@@ -167,14 +167,14 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
|
||||
}
|
||||
query.MetricQueryType = models.MetricQueryTypeQuery
|
||||
|
||||
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{
|
||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value1")}}},
|
||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value2")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value2")}}},
|
||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value3")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value3")}}},
|
||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value4")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value4")}}},
|
||||
api := &mocks.MetricsAPI{Metrics: []cloudwatchtypes.Metric{
|
||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value1")}}},
|
||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value2")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value2")}}},
|
||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value3")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value3")}}},
|
||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []cloudwatchtypes.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value4")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value4")}}},
|
||||
}}
|
||||
api.On("ListMetricsPagesWithContext").Return(nil)
|
||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
|
||||
api.On("ListMetrics").Return(nil)
|
||||
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, queries, 1)
|
||||
assert.Equal(t, map[string][]string{
|
||||
@@ -190,7 +190,7 @@ func TestGetDimensionValuesForWildcards(t *testing.T) {
|
||||
query.Dimensions = map[string][]string{}
|
||||
query.MetricQueryType = models.MetricQueryTypeQuery
|
||||
|
||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
|
||||
queries, err := ds.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, queries, 1)
|
||||
assert.Equal(t, map[string][]string{}, queries[0].Dimensions)
|
||||
|
||||
@@ -4,15 +4,16 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
|
||||
)
|
||||
|
||||
func (e *cloudWatchExecutor) executeRequest(ctx context.Context, client cloudwatchiface.CloudWatchAPI,
|
||||
func (ds *DataSource) executeRequest(ctx context.Context, client models.CWClient,
|
||||
metricDataInput *cloudwatch.GetMetricDataInput) ([]*cloudwatch.GetMetricDataOutput, error) {
|
||||
mdo := make([]*cloudwatch.GetMetricDataOutput, 0)
|
||||
|
||||
@@ -26,7 +27,7 @@ func (e *cloudWatchExecutor) executeRequest(ctx context.Context, client cloudwat
|
||||
*metricDataInput.EndTime = metricDataInput.EndTime.Truncate(time.Minute).Add(time.Minute)
|
||||
}
|
||||
|
||||
resp, err := client.GetMetricDataWithContext(ctx, metricDataInput)
|
||||
resp, err := client.GetMetricData(ctx, metricDataInput)
|
||||
if err != nil {
|
||||
return mdo, backend.DownstreamError(err)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
|
||||
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -16,39 +18,39 @@ import (
|
||||
|
||||
func TestGetMetricDataExecutorTestRequest(t *testing.T) {
|
||||
t.Run("Should round up end time if cloudWatchRoundUpEndTime is enabled", func(t *testing.T) {
|
||||
executor := &cloudWatchExecutor{}
|
||||
executor := &DataSource{}
|
||||
queryEndTime, _ := time.Parse("2006-01-02T15:04:05Z07:00", "2024-05-01T01:45:04Z")
|
||||
inputs := &cloudwatch.GetMetricDataInput{EndTime: &queryEndTime, MetricDataQueries: []*cloudwatch.MetricDataQuery{}}
|
||||
inputs := &cloudwatch.GetMetricDataInput{EndTime: &queryEndTime, MetricDataQueries: []cloudwatchtypes.MetricDataQuery{}}
|
||||
mockMetricClient := &mocks.MetricsAPI{}
|
||||
mockMetricClient.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(
|
||||
mockMetricClient.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(
|
||||
&cloudwatch.GetMetricDataOutput{
|
||||
MetricDataResults: []*cloudwatch.MetricDataResult{{Values: []*float64{}}},
|
||||
MetricDataResults: []cloudwatchtypes.MetricDataResult{{Values: []float64{}}},
|
||||
}, nil).Once()
|
||||
_, err := executor.executeRequest(contextWithFeaturesEnabled(features.FlagCloudWatchRoundUpEndTime), mockMetricClient, inputs)
|
||||
require.NoError(t, err)
|
||||
expectedTime, _ := time.Parse("2006-01-02T15:04:05Z07:00", "2024-05-01T01:46:00Z")
|
||||
expectedInput := &cloudwatch.GetMetricDataInput{EndTime: &expectedTime, MetricDataQueries: []*cloudwatch.MetricDataQuery{}}
|
||||
mockMetricClient.AssertCalled(t, "GetMetricDataWithContext", mock.Anything, expectedInput, mock.Anything)
|
||||
expectedInput := &cloudwatch.GetMetricDataInput{EndTime: &expectedTime, MetricDataQueries: []cloudwatchtypes.MetricDataQuery{}}
|
||||
mockMetricClient.AssertCalled(t, "GetMetricData", mock.Anything, expectedInput, mock.Anything)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetMetricDataExecutorTestResponse(t *testing.T) {
|
||||
executor := &cloudWatchExecutor{}
|
||||
inputs := &cloudwatch.GetMetricDataInput{EndTime: aws.Time(time.Now()), MetricDataQueries: []*cloudwatch.MetricDataQuery{}}
|
||||
executor := &DataSource{}
|
||||
inputs := &cloudwatch.GetMetricDataInput{EndTime: aws.Time(time.Now()), MetricDataQueries: []cloudwatchtypes.MetricDataQuery{}}
|
||||
mockMetricClient := &mocks.MetricsAPI{}
|
||||
mockMetricClient.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(
|
||||
mockMetricClient.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(
|
||||
&cloudwatch.GetMetricDataOutput{
|
||||
MetricDataResults: []*cloudwatch.MetricDataResult{{Values: []*float64{aws.Float64(12.3), aws.Float64(23.5)}}},
|
||||
MetricDataResults: []cloudwatchtypes.MetricDataResult{{Values: []float64{12.3, 23.5}}},
|
||||
NextToken: aws.String("next"),
|
||||
}, nil).Once()
|
||||
mockMetricClient.On("GetMetricDataWithContext", mock.Anything, mock.Anything, mock.Anything).Return(
|
||||
mockMetricClient.On("GetMetricData", mock.Anything, mock.Anything, mock.Anything).Return(
|
||||
&cloudwatch.GetMetricDataOutput{
|
||||
MetricDataResults: []*cloudwatch.MetricDataResult{{Values: []*float64{aws.Float64(100)}}},
|
||||
MetricDataResults: []cloudwatchtypes.MetricDataResult{{Values: []float64{100}}},
|
||||
}, nil).Once()
|
||||
res, err := executor.executeRequest(context.Background(), mockMetricClient, inputs)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 2)
|
||||
require.Len(t, res[0].MetricDataResults[0].Values, 2)
|
||||
assert.Equal(t, 23.5, *res[0].MetricDataResults[0].Values[1])
|
||||
assert.Equal(t, 100.0, *res[1].MetricDataResults[0].Values[0])
|
||||
assert.Equal(t, 23.5, res[0].MetricDataResults[0].Values[1])
|
||||
assert.Equal(t, 100.0, res[1].MetricDataResults[0].Values[0])
|
||||
}
|
||||
|
||||
@@ -10,11 +10,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
||||
"github.com/aws/smithy-go"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
|
||||
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -25,10 +26,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
limitExceededException = "LimitExceededException"
|
||||
throttlingException = "ThrottlingException"
|
||||
defaultEventLimit = int64(10)
|
||||
defaultLogGroupLimit = int64(50)
|
||||
defaultEventLimit = int32(10)
|
||||
defaultLogGroupLimit = int32(50)
|
||||
logIdentifierInternal = "__log__grafana_internal__"
|
||||
logStreamIdentifierInternal = "__logstream__grafana_internal__"
|
||||
)
|
||||
@@ -43,46 +42,7 @@ func (e *AWSError) Error() string {
|
||||
return fmt.Sprintf("CloudWatch error: %s: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// StartQueryInputWithLanguage copies the StartQueryInput struct from aws-sdk-go@v1.55.5
|
||||
// (https://github.com/aws/aws-sdk-go/blob/7112c0a0c2d01713a9db2d57f0e5722225baf5b5/service/cloudwatchlogs/api.go#L19541)
|
||||
// to add support for the new QueryLanguage parameter, which is unlikely to be backported
|
||||
// since v1 of the aws-sdk-go is in maintenance mode. We've removed the comments for
|
||||
// clarity.
|
||||
type StartQueryInputWithLanguage struct {
|
||||
_ struct{} `type:"structure"`
|
||||
|
||||
EndTime *int64 `locationName:"endTime" type:"long" required:"true"`
|
||||
Limit *int64 `locationName:"limit" min:"1" type:"integer"`
|
||||
LogGroupIdentifiers []*string `locationName:"logGroupIdentifiers" type:"list"`
|
||||
LogGroupName *string `locationName:"logGroupName" min:"1" type:"string"`
|
||||
LogGroupNames []*string `locationName:"logGroupNames" type:"list"`
|
||||
QueryString *string `locationName:"queryString" type:"string" required:"true"`
|
||||
// QueryLanguage is the only change here from the original code.
|
||||
QueryLanguage *string `locationName:"queryLanguage" type:"string"`
|
||||
StartTime *int64 `locationName:"startTime" type:"long" required:"true"`
|
||||
}
|
||||
type WithQueryLanguageFunc func(language *dataquery.LogsQueryLanguage) func(*request.Request)
|
||||
|
||||
// WithQueryLanguage assigns the function to a variable in order to mock it in log_actions_test.go
|
||||
var WithQueryLanguage WithQueryLanguageFunc = withQueryLanguage
|
||||
|
||||
func withQueryLanguage(language *dataquery.LogsQueryLanguage) func(request *request.Request) {
|
||||
return func(request *request.Request) {
|
||||
sqi := request.Params.(*cloudwatchlogs.StartQueryInput)
|
||||
request.Params = &StartQueryInputWithLanguage{
|
||||
EndTime: sqi.EndTime,
|
||||
Limit: sqi.Limit,
|
||||
LogGroupIdentifiers: sqi.LogGroupIdentifiers,
|
||||
LogGroupName: sqi.LogGroupName,
|
||||
LogGroupNames: sqi.LogGroupNames,
|
||||
QueryString: sqi.QueryString,
|
||||
QueryLanguage: (*string)(language),
|
||||
StartTime: sqi.StartTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
func (ds *DataSource) executeLogActions(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
resp := backend.NewQueryDataResponse()
|
||||
|
||||
resultChan := make(chan backend.Responses, len(req.Queries))
|
||||
@@ -97,7 +57,7 @@ func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend
|
||||
|
||||
query := query
|
||||
eg.Go(func() error {
|
||||
dataframe, err := e.executeLogAction(ectx, logsQuery, query, req.PluginContext)
|
||||
dataframe, err := ds.executeLogAction(ectx, logsQuery, query)
|
||||
if err != nil {
|
||||
resultChan <- backend.Responses{
|
||||
query.RefID: backend.ErrorResponseWithErrorSource(err),
|
||||
@@ -134,71 +94,64 @@ func (e *cloudWatchExecutor) executeLogActions(ctx context.Context, req *backend
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) executeLogAction(ctx context.Context, logsQuery models.LogsQuery, query backend.DataQuery, pluginCtx backend.PluginContext) (*data.Frame, error) {
|
||||
instance, err := e.getInstance(ctx, pluginCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
region := instance.Settings.Region
|
||||
func (ds *DataSource) executeLogAction(ctx context.Context, logsQuery models.LogsQuery, query backend.DataQuery) (*data.Frame, error) {
|
||||
region := ds.Settings.Region
|
||||
if logsQuery.Region != "" {
|
||||
region = logsQuery.Region
|
||||
}
|
||||
|
||||
logsClient, err := e.getCWLogsClient(ctx, pluginCtx, region)
|
||||
logsClient, err := ds.getCWLogsClient(ctx, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data *data.Frame = nil
|
||||
var frame *data.Frame
|
||||
switch logsQuery.Subtype {
|
||||
case "StartQuery":
|
||||
data, err = e.handleStartQuery(ctx, logsClient, logsQuery, query.TimeRange, query.RefID)
|
||||
frame, err = ds.handleStartQuery(ctx, logsClient, logsQuery, query.TimeRange, query.RefID)
|
||||
case "StopQuery":
|
||||
data, err = e.handleStopQuery(ctx, logsClient, logsQuery)
|
||||
frame, err = ds.handleStopQuery(ctx, logsClient, logsQuery)
|
||||
case "GetQueryResults":
|
||||
data, err = e.handleGetQueryResults(ctx, logsClient, logsQuery, query.RefID)
|
||||
frame, err = ds.handleGetQueryResults(ctx, logsClient, logsQuery, query.RefID)
|
||||
case "GetLogEvents":
|
||||
data, err = e.handleGetLogEvents(ctx, logsClient, logsQuery)
|
||||
frame, err = ds.handleGetLogEvents(ctx, logsClient, logsQuery)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute log action with subtype: %s: %w", logsQuery.Subtype, err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
return frame, nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||
func (ds *DataSource) handleGetLogEvents(ctx context.Context, logsClient models.CWLogsClient,
|
||||
logsQuery models.LogsQuery) (*data.Frame, error) {
|
||||
limit := defaultEventLimit
|
||||
if logsQuery.Limit != nil && *logsQuery.Limit > 0 {
|
||||
limit = *logsQuery.Limit
|
||||
}
|
||||
if logsQuery.LogGroupName == "" {
|
||||
return nil, backend.DownstreamError(fmt.Errorf("parameter 'logGroupName' is required"))
|
||||
}
|
||||
if logsQuery.LogStreamName == "" {
|
||||
return nil, backend.DownstreamError(fmt.Errorf("parameter 'logStreamName' is required"))
|
||||
}
|
||||
|
||||
queryRequest := &cloudwatchlogs.GetLogEventsInput{
|
||||
Limit: aws.Int64(limit),
|
||||
Limit: aws.Int32(limit),
|
||||
StartFromHead: aws.Bool(logsQuery.StartFromHead),
|
||||
LogGroupName: &logsQuery.LogGroupName,
|
||||
LogStreamName: &logsQuery.LogStreamName,
|
||||
}
|
||||
|
||||
if logsQuery.LogGroupName == "" {
|
||||
return nil, backend.DownstreamError(fmt.Errorf("Error: Parameter 'logGroupName' is required"))
|
||||
}
|
||||
queryRequest.SetLogGroupName(logsQuery.LogGroupName)
|
||||
|
||||
if logsQuery.LogStreamName == "" {
|
||||
return nil, backend.DownstreamError(fmt.Errorf("Error: Parameter 'logStreamName' is required"))
|
||||
}
|
||||
queryRequest.SetLogStreamName(logsQuery.LogStreamName)
|
||||
|
||||
if logsQuery.StartTime != nil && *logsQuery.StartTime != 0 {
|
||||
queryRequest.SetStartTime(*logsQuery.StartTime)
|
||||
queryRequest.StartTime = logsQuery.StartTime
|
||||
}
|
||||
|
||||
if logsQuery.EndTime != nil && *logsQuery.EndTime != 0 {
|
||||
queryRequest.SetEndTime(*logsQuery.EndTime)
|
||||
queryRequest.EndTime = logsQuery.EndTime
|
||||
}
|
||||
|
||||
logEvents, err := logsClient.GetLogEventsWithContext(ctx, queryRequest)
|
||||
logEvents, err := logsClient.GetLogEvents(ctx, queryRequest)
|
||||
if err != nil {
|
||||
return nil, backend.DownstreamError(err)
|
||||
}
|
||||
@@ -223,7 +176,7 @@ func (e *cloudWatchExecutor) handleGetLogEvents(ctx context.Context, logsClient
|
||||
return data.NewFrame("logEvents", timestampField, messageField), nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||
func (ds *DataSource) executeStartQuery(ctx context.Context, logsClient models.CWLogsClient,
|
||||
logsQuery models.LogsQuery, timeRange backend.TimeRange) (*cloudwatchlogs.StartQueryOutput, error) {
|
||||
startTime := timeRange.From
|
||||
endTime := timeRange.To
|
||||
@@ -267,36 +220,36 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c
|
||||
// due to a bug in the startQuery api, we remove * from the arn, otherwise it throws an error
|
||||
logGroupIdentifiers = append(logGroupIdentifiers, strings.TrimSuffix(arn, "*"))
|
||||
}
|
||||
startQueryInput.LogGroupIdentifiers = aws.StringSlice(logGroupIdentifiers)
|
||||
startQueryInput.LogGroupIdentifiers = logGroupIdentifiers
|
||||
} else {
|
||||
// even though log group names are being phased out, we still need to support them for backwards compatibility and alert queries
|
||||
startQueryInput.LogGroupNames = aws.StringSlice(logsQuery.LogGroupNames)
|
||||
startQueryInput.LogGroupNames = logsQuery.LogGroupNames
|
||||
}
|
||||
}
|
||||
|
||||
if logsQuery.Limit != nil {
|
||||
startQueryInput.Limit = aws.Int64(*logsQuery.Limit)
|
||||
startQueryInput.Limit = aws.Int32(*logsQuery.Limit)
|
||||
}
|
||||
if logsQuery.QueryLanguage != nil {
|
||||
startQueryInput.QueryLanguage = cloudwatchlogstypes.QueryLanguage(*logsQuery.QueryLanguage)
|
||||
}
|
||||
|
||||
e.logger.FromContext(ctx).Debug("Calling startquery with context with input", "input", startQueryInput)
|
||||
resp, err := logsClient.StartQueryWithContext(ctx, startQueryInput, WithQueryLanguage(logsQuery.QueryLanguage))
|
||||
ds.logger.FromContext(ctx).Debug("Calling startquery with context with input", "input", startQueryInput)
|
||||
resp, err := logsClient.StartQuery(ctx, startQueryInput)
|
||||
if err != nil {
|
||||
var awsErr awserr.Error
|
||||
if errors.As(err, &awsErr) && awsErr.Code() == "LimitExceededException" {
|
||||
e.logger.FromContext(ctx).Debug("ExecuteStartQuery limit exceeded", "err", awsErr)
|
||||
err = &AWSError{Code: limitExceededException, Message: err.Error()}
|
||||
} else if errors.As(err, &awsErr) && awsErr.Code() == "ThrottlingException" {
|
||||
e.logger.FromContext(ctx).Debug("ExecuteStartQuery rate exceeded", "err", awsErr)
|
||||
err = &AWSError{Code: throttlingException, Message: err.Error()}
|
||||
if errors.Is(err, &cloudwatchlogstypes.LimitExceededException{}) {
|
||||
ds.logger.FromContext(ctx).Debug("ExecuteStartQuery limit exceeded", "err", err)
|
||||
} else if errors.Is(err, &cloudwatchlogstypes.ThrottlingException{}) {
|
||||
ds.logger.FromContext(ctx).Debug("ExecuteStartQuery rate exceeded", "err", err)
|
||||
}
|
||||
err = backend.DownstreamError(err)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||
func (ds *DataSource) handleStartQuery(ctx context.Context, logsClient models.CWLogsClient,
|
||||
logsQuery models.LogsQuery, timeRange backend.TimeRange, refID string) (*data.Frame, error) {
|
||||
startQueryResponse, err := e.executeStartQuery(ctx, logsClient, logsQuery, timeRange)
|
||||
startQueryResponse, err := ds.executeStartQuery(ctx, logsClient, logsQuery, timeRange)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -318,20 +271,19 @@ func (e *cloudWatchExecutor) handleStartQuery(ctx context.Context, logsClient cl
|
||||
return dataFrame, nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) executeStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||
func (ds *DataSource) executeStopQuery(ctx context.Context, logsClient models.CWLogsClient,
|
||||
logsQuery models.LogsQuery) (*cloudwatchlogs.StopQueryOutput, error) {
|
||||
queryInput := &cloudwatchlogs.StopQueryInput{
|
||||
QueryId: aws.String(logsQuery.QueryId),
|
||||
}
|
||||
|
||||
response, err := logsClient.StopQueryWithContext(ctx, queryInput)
|
||||
response, err := logsClient.StopQuery(ctx, queryInput)
|
||||
if err != nil {
|
||||
// If the query has already stopped by the time CloudWatch receives the stop query request,
|
||||
// an "InvalidParameterException" error is returned. For our purposes though the query has been
|
||||
// stopped, so we ignore the error.
|
||||
var awsErr awserr.Error
|
||||
if errors.As(err, &awsErr) && awsErr.Code() == "InvalidParameterException" {
|
||||
response = &cloudwatchlogs.StopQueryOutput{Success: aws.Bool(false)}
|
||||
if errors.Is(err, &cloudwatchlogstypes.InvalidParameterException{}) {
|
||||
response = &cloudwatchlogs.StopQueryOutput{Success: false}
|
||||
err = nil
|
||||
} else {
|
||||
err = backend.DownstreamError(err)
|
||||
@@ -341,37 +293,37 @@ func (e *cloudWatchExecutor) executeStopQuery(ctx context.Context, logsClient cl
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) handleStopQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||
func (ds *DataSource) handleStopQuery(ctx context.Context, logsClient models.CWLogsClient,
|
||||
logsQuery models.LogsQuery) (*data.Frame, error) {
|
||||
response, err := e.executeStopQuery(ctx, logsClient, logsQuery)
|
||||
response, err := ds.executeStopQuery(ctx, logsClient, logsQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataFrame := data.NewFrame("StopQueryResponse", data.NewField("success", nil, []bool{*response.Success}))
|
||||
dataFrame := data.NewFrame("StopQueryResponse", data.NewField("success", nil, []bool{response.Success}))
|
||||
return dataFrame, nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) executeGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||
func (ds *DataSource) executeGetQueryResults(ctx context.Context, logsClient models.CWLogsClient,
|
||||
logsQuery models.LogsQuery) (*cloudwatchlogs.GetQueryResultsOutput, error) {
|
||||
queryInput := &cloudwatchlogs.GetQueryResultsInput{
|
||||
QueryId: aws.String(logsQuery.QueryId),
|
||||
}
|
||||
|
||||
getQueryResultsResponse, err := logsClient.GetQueryResultsWithContext(ctx, queryInput)
|
||||
getQueryResultsResponse, err := logsClient.GetQueryResults(ctx, queryInput)
|
||||
if err != nil {
|
||||
var awsErr awserr.Error
|
||||
var awsErr smithy.APIError
|
||||
if errors.As(err, &awsErr) {
|
||||
err = &AWSError{Code: awsErr.Code(), Message: err.Error()}
|
||||
err = &AWSError{Code: awsErr.ErrorCode(), Message: awsErr.ErrorMessage()}
|
||||
}
|
||||
err = backend.DownstreamError(err)
|
||||
}
|
||||
return getQueryResultsResponse, err
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) handleGetQueryResults(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||
func (ds *DataSource) handleGetQueryResults(ctx context.Context, logsClient models.CWLogsClient,
|
||||
logsQuery models.LogsQuery, refID string) (*data.Frame, error) {
|
||||
getQueryResultsOutput, err := e.executeGetQueryResults(ctx, logsClient, logsQuery)
|
||||
getQueryResultsOutput, err := ds.executeGetQueryResults(ctx, logsClient, logsQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -6,19 +6,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
|
||||
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
|
||||
@@ -36,7 +30,7 @@ func TestQuery_handleGetLogEvents_passes_nil_start_and_end_times_to_GetLogEvents
|
||||
|
||||
var cli fakeCWLogsClient
|
||||
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
NewCWLogsClient = func(cfg aws.Config) models.CWLogsClient {
|
||||
return &cli
|
||||
}
|
||||
const refID = "A"
|
||||
@@ -57,7 +51,7 @@ func TestQuery_handleGetLogEvents_passes_nil_start_and_end_times_to_GetLogEvents
|
||||
expectedInput: []*cloudwatchlogs.GetLogEventsInput{
|
||||
{
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int64(10),
|
||||
Limit: aws.Int32(10),
|
||||
LogGroupName: aws.String("foo"),
|
||||
LogStreamName: aws.String("bar"),
|
||||
StartFromHead: aws.Bool(false),
|
||||
@@ -76,7 +70,7 @@ func TestQuery_handleGetLogEvents_passes_nil_start_and_end_times_to_GetLogEvents
|
||||
expectedInput: []*cloudwatchlogs.GetLogEventsInput{
|
||||
{
|
||||
StartTime: aws.Int64(1),
|
||||
Limit: aws.Int64(10),
|
||||
Limit: aws.Int32(10),
|
||||
LogGroupName: aws.String("foo"),
|
||||
LogStreamName: aws.String("bar"),
|
||||
StartFromHead: aws.Bool(true),
|
||||
@@ -88,13 +82,8 @@ func TestQuery_handleGetLogEvents_passes_nil_start_and_end_times_to_GetLogEvents
|
||||
for name, test := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
ds := newTestDatasource()
|
||||
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
|
||||
},
|
||||
@@ -108,8 +97,8 @@ func TestQuery_handleGetLogEvents_passes_nil_start_and_end_times_to_GetLogEvents
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, cli.calls.getEventsWithContext, 1)
|
||||
assert.Equal(t, test.expectedInput, cli.calls.getEventsWithContext)
|
||||
require.Len(t, cli.calls.getEvents, 1)
|
||||
assert.Equal(t, test.expectedInput, cli.calls.getEvents)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -120,22 +109,19 @@ func TestQuery_GetLogEvents_returns_response_from_GetLogEvents_to_data_frame_fie
|
||||
NewCWLogsClient = origNewCWLogsClient
|
||||
})
|
||||
var cli *mocks.MockLogEvents
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
NewCWLogsClient = func(cfg aws.Config) models.CWLogsClient {
|
||||
return cli
|
||||
}
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource()
|
||||
|
||||
cli = &mocks.MockLogEvents{}
|
||||
cli.On("GetLogEventsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetLogEventsOutput{
|
||||
Events: []*cloudwatchlogs.OutputLogEvent{{
|
||||
cli.On("GetLogEvents", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetLogEventsOutput{
|
||||
Events: []cloudwatchlogstypes.OutputLogEvent{{
|
||||
Message: utils.Pointer("some message"),
|
||||
Timestamp: utils.Pointer(int64(15)),
|
||||
}}}, nil)
|
||||
|
||||
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
|
||||
},
|
||||
@@ -174,7 +160,7 @@ func TestQuery_StartQuery(t *testing.T) {
|
||||
|
||||
var cli fakeCWLogsClient
|
||||
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
NewCWLogsClient = func(cfg aws.Config) models.CWLogsClient {
|
||||
return &cli
|
||||
}
|
||||
|
||||
@@ -183,18 +169,18 @@ func TestQuery_StartQuery(t *testing.T) {
|
||||
|
||||
cli = fakeCWLogsClient{
|
||||
logGroupFields: cloudwatchlogs.GetLogGroupFieldsOutput{
|
||||
LogGroupFields: []*cloudwatchlogs.LogGroupField{
|
||||
LogGroupFields: []cloudwatchlogstypes.LogGroupField{
|
||||
{
|
||||
Name: aws.String("field_a"),
|
||||
Percent: aws.Int64(100),
|
||||
Percent: 100,
|
||||
},
|
||||
{
|
||||
Name: aws.String("field_b"),
|
||||
Percent: aws.Int64(30),
|
||||
Percent: 30,
|
||||
},
|
||||
{
|
||||
Name: aws.String("field_c"),
|
||||
Percent: aws.Int64(55),
|
||||
Percent: 55,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -205,16 +191,10 @@ func TestQuery_StartQuery(t *testing.T) {
|
||||
To: time.Unix(1584700643, 0),
|
||||
}
|
||||
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{
|
||||
AWSDatasourceSettings: awsds.AWSDatasourceSettings{
|
||||
Region: "us-east-2",
|
||||
},
|
||||
}, sessions: &fakeSessionCache{}}, nil
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.Region = "us-east-2"
|
||||
})
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
|
||||
},
|
||||
@@ -241,18 +221,18 @@ func TestQuery_StartQuery(t *testing.T) {
|
||||
const refID = "A"
|
||||
cli = fakeCWLogsClient{
|
||||
logGroupFields: cloudwatchlogs.GetLogGroupFieldsOutput{
|
||||
LogGroupFields: []*cloudwatchlogs.LogGroupField{
|
||||
LogGroupFields: []cloudwatchlogstypes.LogGroupField{
|
||||
{
|
||||
Name: aws.String("field_a"),
|
||||
Percent: aws.Int64(100),
|
||||
Percent: 100,
|
||||
},
|
||||
{
|
||||
Name: aws.String("field_b"),
|
||||
Percent: aws.Int64(30),
|
||||
Percent: 30,
|
||||
},
|
||||
{
|
||||
Name: aws.String("field_c"),
|
||||
Percent: aws.Int64(55),
|
||||
Percent: 55,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -263,16 +243,10 @@ func TestQuery_StartQuery(t *testing.T) {
|
||||
To: time.Unix(1584873443000, 0),
|
||||
}
|
||||
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{
|
||||
AWSDatasourceSettings: awsds.AWSDatasourceSettings{
|
||||
Region: "us-east-2",
|
||||
},
|
||||
}, sessions: &fakeSessionCache{}}, nil
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.Region = "us-east-2"
|
||||
})
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
|
||||
},
|
||||
@@ -311,26 +285,6 @@ func TestQuery_StartQuery(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
type withQueryLanguageMock struct {
|
||||
capturedLanguage *dataquery.LogsQueryLanguage
|
||||
mockWithQueryLanguage func(language *dataquery.LogsQueryLanguage) func(request *request.Request)
|
||||
}
|
||||
|
||||
func newWithQueryLanguageMock() *withQueryLanguageMock {
|
||||
mock := &withQueryLanguageMock{
|
||||
capturedLanguage: new(dataquery.LogsQueryLanguage),
|
||||
}
|
||||
|
||||
mock.mockWithQueryLanguage = func(language *dataquery.LogsQueryLanguage) func(request *request.Request) {
|
||||
*mock.capturedLanguage = *language
|
||||
return func(req *request.Request) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
func Test_executeStartQuery(t *testing.T) {
|
||||
origNewCWLogsClient := NewCWLogsClient
|
||||
t.Cleanup(func() {
|
||||
@@ -339,15 +293,15 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
|
||||
var cli fakeCWLogsClient
|
||||
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
NewCWLogsClient = func(cfg aws.Config) models.CWLogsClient {
|
||||
return &cli
|
||||
}
|
||||
|
||||
t.Run("successfully parses information from JSON to StartQueryWithContext for language", func(t *testing.T) {
|
||||
t.Run("successfully parses information from JSON to StartQuery for language", func(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
queries []backend.DataQuery
|
||||
expectedOutput []*cloudwatchlogs.StartQueryInput
|
||||
queryLanguage dataquery.LogsQueryLanguage
|
||||
queryLanguage cloudwatchlogstypes.QueryLanguage
|
||||
}{
|
||||
"not defined": {
|
||||
queries: []backend.DataQuery{
|
||||
@@ -366,11 +320,12 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
expectedOutput: []*cloudwatchlogs.StartQueryInput{{
|
||||
StartTime: aws.Int64(0),
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int64(12),
|
||||
Limit: aws.Int32(12),
|
||||
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
|
||||
LogGroupNames: []*string{aws.String("some name"), aws.String("another name")},
|
||||
LogGroupNames: []string{"some name", "another name"},
|
||||
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
|
||||
}},
|
||||
queryLanguage: dataquery.LogsQueryLanguageCWLI,
|
||||
queryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
|
||||
},
|
||||
"CWLI": {
|
||||
queries: []backend.DataQuery{{
|
||||
@@ -389,12 +344,13 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
{
|
||||
StartTime: aws.Int64(0),
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int64(12),
|
||||
Limit: aws.Int32(12),
|
||||
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
|
||||
LogGroupNames: []*string{aws.String("some name"), aws.String("another name")},
|
||||
LogGroupNames: []string{"some name", "another name"},
|
||||
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
|
||||
},
|
||||
},
|
||||
queryLanguage: dataquery.LogsQueryLanguageCWLI,
|
||||
queryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
|
||||
},
|
||||
"PPL": {
|
||||
queries: []backend.DataQuery{{
|
||||
@@ -413,12 +369,13 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
{
|
||||
StartTime: aws.Int64(0),
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int64(12),
|
||||
Limit: aws.Int32(12),
|
||||
QueryString: aws.String("source logs | fields @message"),
|
||||
LogGroupNames: []*string{aws.String("some name"), aws.String("another name")},
|
||||
LogGroupNames: []string{"some name", "another name"},
|
||||
QueryLanguage: cloudwatchlogstypes.QueryLanguagePpl,
|
||||
},
|
||||
},
|
||||
queryLanguage: dataquery.LogsQueryLanguagePPL,
|
||||
queryLanguage: cloudwatchlogstypes.QueryLanguagePpl,
|
||||
},
|
||||
"SQL": {
|
||||
queries: []backend.DataQuery{
|
||||
@@ -439,49 +396,35 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
{
|
||||
StartTime: aws.Int64(0),
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int64(12),
|
||||
Limit: aws.Int32(12),
|
||||
QueryString: aws.String("SELECT * FROM logs"),
|
||||
LogGroupNames: nil,
|
||||
QueryLanguage: cloudwatchlogstypes.QueryLanguageSql,
|
||||
},
|
||||
},
|
||||
queryLanguage: dataquery.LogsQueryLanguageSQL,
|
||||
queryLanguage: cloudwatchlogstypes.QueryLanguageSql,
|
||||
},
|
||||
}
|
||||
for name, test := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
|
||||
languageMock := newWithQueryLanguageMock()
|
||||
originalWithQueryLanguage := WithQueryLanguage
|
||||
WithQueryLanguage = languageMock.mockWithQueryLanguage
|
||||
defer func() {
|
||||
WithQueryLanguage = originalWithQueryLanguage
|
||||
}()
|
||||
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
ds := newTestDatasource()
|
||||
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: test.queries,
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.expectedOutput, cli.calls.startQueryWithContext)
|
||||
assert.Equal(t, &test.queryLanguage, languageMock.capturedLanguage)
|
||||
assert.Equal(t, test.expectedOutput, cli.calls.startQuery)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("does not populate StartQueryInput.limit when no limit provided", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource()
|
||||
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
{
|
||||
@@ -496,18 +439,15 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, cli.calls.startQueryWithContext, 1)
|
||||
assert.Nil(t, cli.calls.startQueryWithContext[0].Limit)
|
||||
require.Len(t, cli.calls.startQuery, 1)
|
||||
assert.Nil(t, cli.calls.startQuery[0].Limit)
|
||||
})
|
||||
|
||||
t.Run("attaches logGroupIdentifiers if the crossAccount feature is enabled", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource()
|
||||
|
||||
_, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
|
||||
_, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
{
|
||||
@@ -530,21 +470,19 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
{
|
||||
StartTime: aws.Int64(0),
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int64(12),
|
||||
Limit: aws.Int32(12),
|
||||
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
|
||||
LogGroupIdentifiers: []*string{aws.String("fakeARN")},
|
||||
LogGroupIdentifiers: []string{"fakeARN"},
|
||||
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
|
||||
},
|
||||
}, cli.calls.startQueryWithContext)
|
||||
}, cli.calls.startQuery)
|
||||
})
|
||||
|
||||
t.Run("attaches logGroupIdentifiers if the crossAccount feature is enabled and strips out trailing *", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource()
|
||||
|
||||
_, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
|
||||
_, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
{
|
||||
@@ -566,20 +504,18 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
{
|
||||
StartTime: aws.Int64(0),
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int64(12),
|
||||
Limit: aws.Int32(12),
|
||||
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
|
||||
LogGroupIdentifiers: []*string{aws.String("*fake**ARN")},
|
||||
LogGroupIdentifiers: []string{"*fake**ARN"},
|
||||
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
|
||||
},
|
||||
}, cli.calls.startQueryWithContext)
|
||||
}, cli.calls.startQuery)
|
||||
})
|
||||
|
||||
t.Run("uses LogGroupNames if the cross account feature flag is not enabled, and log group names is present", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
ds := newTestDatasource()
|
||||
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
{
|
||||
@@ -601,20 +537,18 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
{
|
||||
StartTime: aws.Int64(0),
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int64(12),
|
||||
Limit: aws.Int32(12),
|
||||
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
|
||||
LogGroupNames: []*string{aws.String("/log-group-name")},
|
||||
LogGroupNames: []string{"/log-group-name"},
|
||||
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
|
||||
},
|
||||
}, cli.calls.startQueryWithContext)
|
||||
}, cli.calls.startQuery)
|
||||
})
|
||||
|
||||
t.Run("ignores logGroups if feature flag is disabled even if logGroupNames is not present", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
ds := newTestDatasource()
|
||||
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
{
|
||||
@@ -635,20 +569,18 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
{
|
||||
StartTime: aws.Int64(0),
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int64(12),
|
||||
Limit: aws.Int32(12),
|
||||
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
|
||||
LogGroupNames: []*string{},
|
||||
LogGroupNames: nil,
|
||||
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
|
||||
},
|
||||
}, cli.calls.startQueryWithContext)
|
||||
}, cli.calls.startQuery)
|
||||
})
|
||||
|
||||
t.Run("it always uses logGroups when feature flag is enabled and ignores log group names", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{}
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
_, err := executor.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
|
||||
ds := newTestDatasource()
|
||||
_, err := ds.QueryData(contextWithFeaturesEnabled(features.FlagCloudWatchCrossAccountQuerying), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
{
|
||||
@@ -670,11 +602,12 @@ func Test_executeStartQuery(t *testing.T) {
|
||||
{
|
||||
StartTime: aws.Int64(0),
|
||||
EndTime: aws.Int64(1),
|
||||
Limit: aws.Int64(12),
|
||||
Limit: aws.Int32(12),
|
||||
QueryString: aws.String("fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|fields @message"),
|
||||
LogGroupIdentifiers: []*string{aws.String("*fake**ARN")},
|
||||
LogGroupIdentifiers: []string{"*fake**ARN"},
|
||||
QueryLanguage: cloudwatchlogstypes.QueryLanguageCwli,
|
||||
},
|
||||
}, cli.calls.startQueryWithContext)
|
||||
}, cli.calls.startQuery)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -686,40 +619,36 @@ func TestQuery_StopQuery(t *testing.T) {
|
||||
|
||||
var cli fakeCWLogsClient
|
||||
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
NewCWLogsClient = func(aws.Config) models.CWLogsClient {
|
||||
return &cli
|
||||
}
|
||||
|
||||
cli = fakeCWLogsClient{
|
||||
logGroupFields: cloudwatchlogs.GetLogGroupFieldsOutput{
|
||||
LogGroupFields: []*cloudwatchlogs.LogGroupField{
|
||||
LogGroupFields: []cloudwatchlogstypes.LogGroupField{
|
||||
{
|
||||
Name: aws.String("field_a"),
|
||||
Percent: aws.Int64(100),
|
||||
Percent: 100,
|
||||
},
|
||||
{
|
||||
Name: aws.String("field_b"),
|
||||
Percent: aws.Int64(30),
|
||||
Percent: 30,
|
||||
},
|
||||
{
|
||||
Name: aws.String("field_c"),
|
||||
Percent: aws.Int64(55),
|
||||
Percent: 55,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
|
||||
timeRange := backend.TimeRange{
|
||||
From: time.Unix(1584873443, 0),
|
||||
To: time.Unix(1584700643, 0),
|
||||
}
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
ds := newTestDatasource()
|
||||
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
|
||||
},
|
||||
@@ -758,14 +687,14 @@ func TestQuery_GetQueryResults(t *testing.T) {
|
||||
|
||||
var cli fakeCWLogsClient
|
||||
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
NewCWLogsClient = func(aws.Config) models.CWLogsClient {
|
||||
return &cli
|
||||
}
|
||||
|
||||
const refID = "A"
|
||||
cli = fakeCWLogsClient{
|
||||
queryResults: cloudwatchlogs.GetQueryResultsOutput{
|
||||
Results: [][]*cloudwatchlogs.ResultField{
|
||||
Results: [][]cloudwatchlogstypes.ResultField{
|
||||
{
|
||||
{
|
||||
Field: aws.String("@timestamp"),
|
||||
@@ -795,21 +724,17 @@ func TestQuery_GetQueryResults(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
Statistics: &cloudwatchlogs.QueryStatistics{
|
||||
BytesScanned: aws.Float64(512),
|
||||
RecordsMatched: aws.Float64(256),
|
||||
RecordsScanned: aws.Float64(1024),
|
||||
Statistics: &cloudwatchlogstypes.QueryStatistics{
|
||||
BytesScanned: 512,
|
||||
RecordsMatched: 256,
|
||||
RecordsScanned: 1024,
|
||||
},
|
||||
Status: aws.String("Complete"),
|
||||
Status: "Complete",
|
||||
},
|
||||
}
|
||||
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
ds := newTestDatasource()
|
||||
resp, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
|
||||
},
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
package routes
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
|
||||
)
|
||||
|
||||
func TestLogGroupFieldsRoute(t *testing.T) {
|
||||
reqCtxFunc := func(_ context.Context, pluginCtx backend.PluginContext, region string) (reqCtx models.RequestContext, err error) {
|
||||
return models.RequestContext{}, err
|
||||
}
|
||||
origLogGroupsService := services.NewLogGroupsService
|
||||
t.Cleanup(func() {
|
||||
services.NewLogGroupsService = origLogGroupsService
|
||||
})
|
||||
t.Run("returns 400 if an invalid LogGroupFieldsRequest is used", func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", `/log-group-fields?region=us-east-2`, nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupFieldsHandler, logger, nil))
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupFieldsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Equal(t, `{"Message":"error in LogGroupFieldsHandler: you need to specify either logGroupName or logGroupArn","Error":"you need to specify either logGroupName or logGroupArn","StatusCode":400}`, rr.Body.String())
|
||||
@@ -31,14 +32,15 @@ func TestLogGroupFieldsRoute(t *testing.T) {
|
||||
|
||||
t.Run("returns 500 if GetLogGroupFields method fails", func(t *testing.T) {
|
||||
mockLogsService := mocks.LogsService{}
|
||||
mockLogsService.On("GetLogGroupFieldsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroupField]{}, fmt.Errorf("error from api"))
|
||||
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
|
||||
return &mockLogsService, nil
|
||||
mockLogsService.On("GetLogGroupFields", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroupField]{}, fmt.Errorf("error from api"))
|
||||
services.NewLogGroupsService = func(_ models.CloudWatchLogsAPIProvider, _ bool) models.LogGroupsProvider {
|
||||
return &mockLogsService
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/log-group-fields?region=us-east-2&logGroupName=test", nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupFieldsHandler, logger, reqCtxFunc))
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupFieldsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
@@ -47,7 +49,7 @@ func TestLogGroupFieldsRoute(t *testing.T) {
|
||||
|
||||
t.Run("returns valid json response if everything is ok", func(t *testing.T) {
|
||||
mockLogsService := mocks.LogsService{}
|
||||
mockLogsService.On("GetLogGroupFieldsWithContext", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroupField]{
|
||||
mockLogsService.On("GetLogGroupFields", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroupField]{
|
||||
{
|
||||
AccountId: new(string),
|
||||
Value: resources.LogGroupField{
|
||||
@@ -63,13 +65,14 @@ func TestLogGroupFieldsRoute(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
newLogGroupsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.LogGroupsProvider, error) {
|
||||
return &mockLogsService, nil
|
||||
services.NewLogGroupsService = func(_ models.CloudWatchLogsAPIProvider, _ bool) models.LogGroupsProvider {
|
||||
return &mockLogsService
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/log-group-fields?region=us-east-2&logGroupName=test", nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(LogGroupFieldsHandler, logger, reqCtxFunc))
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupFieldsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
219
pkg/tsdb/cloudwatch/log_groups_test.go
Normal file
219
pkg/tsdb/cloudwatch/log_groups_test.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
|
||||
)
|
||||
|
||||
func TestLogGroupsRoute(t *testing.T) {
|
||||
origLogGroupsService := services.NewLogGroupsService
|
||||
t.Cleanup(func() {
|
||||
services.NewLogGroupsService = origLogGroupsService
|
||||
})
|
||||
|
||||
var mockLogsService = mocks.LogsService{}
|
||||
services.NewLogGroupsService = func(models.CloudWatchLogsAPIProvider, bool) models.LogGroupsProvider {
|
||||
return &mockLogsService
|
||||
}
|
||||
|
||||
t.Run("successfully returns 1 log group with account id", func(t *testing.T) {
|
||||
mockLogsService = mocks.LogsService{}
|
||||
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{{
|
||||
Value: resources.LogGroup{
|
||||
Arn: "some arn",
|
||||
Name: "some name",
|
||||
},
|
||||
AccountId: utils.Pointer("111"),
|
||||
}}, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/log-groups", nil)
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.JSONEq(t, `[{"value":{"name":"some name", "arn":"some arn"},"accountId":"111"}]`, rr.Body.String())
|
||||
})
|
||||
|
||||
t.Run("successfully returns multiple log groups with account id", func(t *testing.T) {
|
||||
mockLogsService = mocks.LogsService{}
|
||||
mockLogsService.On("GetLogGroups", mock.Anything).Return(
|
||||
[]resources.ResourceResponse[resources.LogGroup]{
|
||||
{
|
||||
Value: resources.LogGroup{
|
||||
Arn: "arn 1",
|
||||
Name: "name 1",
|
||||
},
|
||||
AccountId: utils.Pointer("111"),
|
||||
}, {
|
||||
Value: resources.LogGroup{
|
||||
Arn: "arn 2",
|
||||
Name: "name 2",
|
||||
},
|
||||
AccountId: utils.Pointer("222"),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/log-groups", nil)
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.JSONEq(t, `[
|
||||
{
|
||||
"value":{
|
||||
"name":"name 1",
|
||||
"arn":"arn 1"
|
||||
},
|
||||
"accountId":"111"
|
||||
},
|
||||
{
|
||||
"value":{
|
||||
"name":"name 2",
|
||||
"arn":"arn 2"
|
||||
},
|
||||
"accountId":"222"
|
||||
}
|
||||
]`, rr.Body.String())
|
||||
})
|
||||
|
||||
t.Run("returns error when both logGroupPrefix and logGroup Pattern are provided", func(t *testing.T) {
|
||||
mockLogsService = mocks.LogsService{}
|
||||
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/log-groups?logGroupNamePrefix=some-prefix&logGroupPattern=some-pattern", nil)
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.JSONEq(t, `{"Error":"cannot set both log group name prefix and pattern", "Message":"cannot set both log group name prefix and pattern: cannot set both log group name prefix and pattern", "StatusCode":400}`, rr.Body.String())
|
||||
})
|
||||
|
||||
t.Run("passes default log group limit and nil for logGroupNamePrefix, accountId, and logGroupPattern", func(t *testing.T) {
|
||||
mockLogsService = mocks.LogsService{}
|
||||
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/log-groups", nil)
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{
|
||||
Limit: 50,
|
||||
ResourceRequest: resources.ResourceRequest{},
|
||||
LogGroupNamePrefix: nil,
|
||||
LogGroupNamePattern: nil,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("passes default log group limit and nil for logGroupNamePrefix when both are absent", func(t *testing.T) {
|
||||
mockLogsService = mocks.LogsService{}
|
||||
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/log-groups", nil)
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{
|
||||
Limit: 50,
|
||||
LogGroupNamePrefix: nil,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("passes log group limit from query parameter", func(t *testing.T) {
|
||||
mockLogsService = mocks.LogsService{}
|
||||
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/log-groups?limit=2", nil)
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{
|
||||
Limit: 2,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("passes logGroupPrefix from query parameter", func(t *testing.T) {
|
||||
mockLogsService = mocks.LogsService{}
|
||||
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/log-groups?logGroupNamePrefix=some-prefix", nil)
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{
|
||||
Limit: 50,
|
||||
LogGroupNamePrefix: utils.Pointer("some-prefix"),
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("passes logGroupPattern from query parameter", func(t *testing.T) {
|
||||
mockLogsService = mocks.LogsService{}
|
||||
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/log-groups?logGroupPattern=some-pattern", nil)
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{
|
||||
Limit: 50,
|
||||
LogGroupNamePattern: utils.Pointer("some-pattern"),
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("passes logGroupPattern from query parameter", func(t *testing.T) {
|
||||
mockLogsService = mocks.LogsService{}
|
||||
mockLogsService.On("GetLogGroups", mock.Anything).Return([]resources.ResourceResponse[resources.LogGroup]{}, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/log-groups?accountId=some-account-id", nil)
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
mockLogsService.AssertCalled(t, "GetLogGroups", resources.LogGroupsRequest{
|
||||
Limit: 50,
|
||||
ResourceRequest: resources.ResourceRequest{AccountId: utils.Pointer("some-account-id")},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("returns error if service returns error", func(t *testing.T) {
|
||||
mockLogsService = mocks.LogsService{}
|
||||
mockLogsService.On("GetLogGroups", mock.Anything).
|
||||
Return([]resources.ResourceResponse[resources.LogGroup]{}, fmt.Errorf("some error"))
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/log-groups", nil)
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.LogGroupsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
assert.JSONEq(t, `{"Error":"some error","Message":"GetLogGroups error: some error","StatusCode":500}`, rr.Body.String())
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
|
||||
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
)
|
||||
|
||||
@@ -18,7 +20,7 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput, gro
|
||||
return nil, fmt.Errorf("response is nil, cannot convert log results to data frames")
|
||||
}
|
||||
|
||||
nonEmptyRows := make([][]*cloudwatchlogs.ResultField, 0)
|
||||
nonEmptyRows := make([][]cloudwatchlogstypes.ResultField, 0)
|
||||
for _, row := range response.Results {
|
||||
// Sometimes CloudWatch can send empty rows
|
||||
if len(row) == 0 {
|
||||
@@ -116,26 +118,20 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput, gro
|
||||
|
||||
queryStats := make([]data.QueryStat, 0)
|
||||
if response.Statistics != nil {
|
||||
if response.Statistics.BytesScanned != nil {
|
||||
queryStats = append(queryStats, data.QueryStat{
|
||||
FieldConfig: data.FieldConfig{DisplayName: "Bytes scanned"},
|
||||
Value: *response.Statistics.BytesScanned,
|
||||
})
|
||||
}
|
||||
queryStats = append(queryStats, data.QueryStat{
|
||||
FieldConfig: data.FieldConfig{DisplayName: "Bytes scanned"},
|
||||
Value: response.Statistics.BytesScanned,
|
||||
})
|
||||
|
||||
if response.Statistics.RecordsScanned != nil {
|
||||
queryStats = append(queryStats, data.QueryStat{
|
||||
FieldConfig: data.FieldConfig{DisplayName: "Records scanned"},
|
||||
Value: *response.Statistics.RecordsScanned,
|
||||
})
|
||||
}
|
||||
queryStats = append(queryStats, data.QueryStat{
|
||||
FieldConfig: data.FieldConfig{DisplayName: "Records scanned"},
|
||||
Value: response.Statistics.RecordsScanned,
|
||||
})
|
||||
|
||||
if response.Statistics.RecordsMatched != nil {
|
||||
queryStats = append(queryStats, data.QueryStat{
|
||||
FieldConfig: data.FieldConfig{DisplayName: "Records matched"},
|
||||
Value: *response.Statistics.RecordsMatched,
|
||||
})
|
||||
}
|
||||
queryStats = append(queryStats, data.QueryStat{
|
||||
FieldConfig: data.FieldConfig{DisplayName: "Records matched"},
|
||||
Value: response.Statistics.RecordsMatched,
|
||||
})
|
||||
}
|
||||
|
||||
frame := data.NewFrame("CloudWatchLogsResponse", newFields...)
|
||||
@@ -148,10 +144,8 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput, gro
|
||||
frame.Meta.Stats = queryStats
|
||||
}
|
||||
|
||||
if response.Status != nil {
|
||||
frame.Meta.Custom = map[string]any{
|
||||
"Status": *response.Status,
|
||||
}
|
||||
frame.Meta.Custom = map[string]any{
|
||||
"Status": string(response.Status),
|
||||
}
|
||||
|
||||
// Results aren't guaranteed to come ordered by time (ascending), so we need to sort
|
||||
@@ -159,7 +153,7 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput, gro
|
||||
return frame, nil
|
||||
}
|
||||
|
||||
func changeToStringField(lengthOfValues int, rows [][]*cloudwatchlogs.ResultField, logEventField string) []*string {
|
||||
func changeToStringField(lengthOfValues int, rows [][]cloudwatchlogstypes.ResultField, logEventField string) []*string {
|
||||
fieldValuesAsStrings := make([]*string, lengthOfValues)
|
||||
for i, resultFields := range rows {
|
||||
for _, field := range resultFields {
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
|
||||
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -19,63 +21,63 @@ import (
|
||||
|
||||
func TestLogsResultsToDataframes(t *testing.T) {
|
||||
fakeCloudwatchResponse := &cloudwatchlogs.GetQueryResultsOutput{
|
||||
Results: [][]*cloudwatchlogs.ResultField{
|
||||
Results: [][]cloudwatchlogstypes.ResultField{
|
||||
{
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@ptr"),
|
||||
Value: aws.String("fake ptr"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@timestamp"),
|
||||
Value: aws.String("2020-03-02 15:04:05.000"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("line"),
|
||||
Value: aws.String("test message 1"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@logStream"),
|
||||
Value: aws.String("fakelogstream"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@log"),
|
||||
Value: aws.String("fakelog"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String(logStreamIdentifierInternal),
|
||||
Value: aws.String("fakelogstream"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String(logIdentifierInternal),
|
||||
Value: aws.String("fakelog"),
|
||||
},
|
||||
},
|
||||
{
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@ptr"),
|
||||
Value: aws.String("fake ptr"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@timestamp"),
|
||||
Value: aws.String("2020-03-02 16:04:05.000"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("line"),
|
||||
Value: aws.String("test message 2"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@logStream"),
|
||||
Value: aws.String("fakelogstream"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@log"),
|
||||
Value: aws.String("fakelog"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String(logStreamIdentifierInternal),
|
||||
Value: aws.String("fakelogstream"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String(logIdentifierInternal),
|
||||
Value: aws.String("fakelog"),
|
||||
},
|
||||
@@ -84,47 +86,47 @@ func TestLogsResultsToDataframes(t *testing.T) {
|
||||
{},
|
||||
// or rows with only timestamp
|
||||
{
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@timestamp"),
|
||||
Value: aws.String("2020-03-02 17:04:05.000"),
|
||||
},
|
||||
},
|
||||
{
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@ptr"),
|
||||
Value: aws.String("fake ptr"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@timestamp"),
|
||||
Value: aws.String("2020-03-02 17:04:05.000"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("line"),
|
||||
Value: aws.String("test message 3"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@logStream"),
|
||||
Value: aws.String("fakelogstream"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@log"),
|
||||
Value: aws.String("fakelog"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String(logStreamIdentifierInternal),
|
||||
Value: aws.String("fakelogstream"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String(logIdentifierInternal),
|
||||
Value: aws.String("fakelog"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: aws.String("ok"),
|
||||
Statistics: &cloudwatchlogs.QueryStatistics{
|
||||
BytesScanned: aws.Float64(2000),
|
||||
RecordsMatched: aws.Float64(3),
|
||||
RecordsScanned: aws.Float64(5000),
|
||||
Status: "ok",
|
||||
Statistics: &cloudwatchlogstypes.QueryStatistics{
|
||||
BytesScanned: 2000,
|
||||
RecordsMatched: 3,
|
||||
RecordsScanned: 5000,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -224,33 +226,33 @@ func TestLogsResultsToDataframes(t *testing.T) {
|
||||
|
||||
func TestLogsResultsToDataframes_MixedTypes_NumericValuesMixedWithStringFallBackToStringValues(t *testing.T) {
|
||||
dataframes, err := logsResultsToDataframes(&cloudwatchlogs.GetQueryResultsOutput{
|
||||
Results: [][]*cloudwatchlogs.ResultField{
|
||||
Results: [][]cloudwatchlogstypes.ResultField{
|
||||
{
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("numberOrString"),
|
||||
Value: aws.String("-1.234"),
|
||||
},
|
||||
},
|
||||
{
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("numberOrString"),
|
||||
Value: aws.String("1"),
|
||||
},
|
||||
},
|
||||
{
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("numberOrString"),
|
||||
Value: aws.String("not a number"),
|
||||
},
|
||||
},
|
||||
{
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("numberOrString"),
|
||||
Value: aws.String("2.000"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: aws.String("ok"),
|
||||
Status: "ok",
|
||||
}, []string{})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -284,27 +286,27 @@ func TestLogsResultsToDataframes_With_Millisecond_Timestamps(t *testing.T) {
|
||||
ingestionTimeField := int64(1732790372916)
|
||||
|
||||
dataframes, err := logsResultsToDataframes(&cloudwatchlogs.GetQueryResultsOutput{
|
||||
Results: [][]*cloudwatchlogs.ResultField{
|
||||
Results: [][]cloudwatchlogstypes.ResultField{
|
||||
{
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@timestamp"),
|
||||
Value: aws.String(fmt.Sprintf("%d", timestampField)),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@ingestionTime"),
|
||||
Value: aws.String(fmt.Sprintf("%d", ingestionTimeField)),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("stringTimeField"),
|
||||
Value: aws.String(stringTimeField),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("message"),
|
||||
Value: aws.String("log message"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: aws.String("ok"),
|
||||
Status: "ok",
|
||||
}, []string{})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -348,23 +350,23 @@ func TestLogsResultsToDataframes_With_Int_Grouping_Field(t *testing.T) {
|
||||
timestampField := int64(1732749534876)
|
||||
|
||||
dataframes, err := logsResultsToDataframes(&cloudwatchlogs.GetQueryResultsOutput{
|
||||
Results: [][]*cloudwatchlogs.ResultField{
|
||||
Results: [][]cloudwatchlogstypes.ResultField{
|
||||
{
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("@timestamp"),
|
||||
Value: aws.String(fmt.Sprintf("%d", timestampField)),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("numberField"),
|
||||
Value: aws.String("8"),
|
||||
},
|
||||
&cloudwatchlogs.ResultField{
|
||||
cloudwatchlogstypes.ResultField{
|
||||
Field: aws.String("groupingNumber"),
|
||||
Value: aws.String("100"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: aws.String("ok"),
|
||||
Status: "ok",
|
||||
}, []string{"groupingNumber"})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery"
|
||||
@@ -17,15 +17,9 @@ import (
|
||||
|
||||
const initialAlertPollPeriod = time.Second
|
||||
|
||||
var executeSyncLogQuery = func(ctx context.Context, e *cloudWatchExecutor, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
var executeSyncLogQuery = func(ctx context.Context, ds *DataSource, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
resp := backend.NewQueryDataResponse()
|
||||
|
||||
instance, err := e.getInstance(ctx, req.PluginContext)
|
||||
if err != nil {
|
||||
resp.Responses[req.Queries[0].RefID] = backend.ErrorResponseWithErrorSource(err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
for _, q := range req.Queries {
|
||||
var logsQuery models.LogsQuery
|
||||
err := json.Unmarshal(q.JSON, &logsQuery)
|
||||
@@ -40,10 +34,10 @@ var executeSyncLogQuery = func(ctx context.Context, e *cloudWatchExecutor, req *
|
||||
|
||||
region := logsQuery.Region
|
||||
if region == "" || region == defaultRegion {
|
||||
logsQuery.Region = instance.Settings.Region
|
||||
logsQuery.Region = ds.Settings.Region
|
||||
}
|
||||
|
||||
logsClient, err := e.getCWLogsClient(ctx, req.PluginContext, region)
|
||||
logsClient, err := ds.getCWLogsClient(ctx, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -53,7 +47,7 @@ var executeSyncLogQuery = func(ctx context.Context, e *cloudWatchExecutor, req *
|
||||
refId = q.RefID
|
||||
}
|
||||
|
||||
getQueryResultsOutput, err := e.syncQuery(ctx, logsClient, q, logsQuery, instance.Settings.LogsTimeout.Duration)
|
||||
getQueryResultsOutput, err := ds.syncQuery(ctx, logsClient, q, logsQuery, ds.Settings.LogsTimeout.Duration)
|
||||
var sourceError backend.ErrorWithSource
|
||||
if errors.As(err, &sourceError) {
|
||||
resp.Responses[refId] = backend.ErrorResponseWithErrorSource(sourceError)
|
||||
@@ -86,9 +80,9 @@ var executeSyncLogQuery = func(ctx context.Context, e *cloudWatchExecutor, req *
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) syncQuery(ctx context.Context, logsClient cloudwatchlogsiface.CloudWatchLogsAPI,
|
||||
func (ds *DataSource) syncQuery(ctx context.Context, logsClient models.CWLogsClient,
|
||||
queryContext backend.DataQuery, logsQuery models.LogsQuery, logsTimeout time.Duration) (*cloudwatchlogs.GetQueryResultsOutput, error) {
|
||||
startQueryOutput, err := e.executeStartQuery(ctx, logsClient, logsQuery, queryContext.TimeRange)
|
||||
startQueryOutput, err := ds.executeStartQuery(ctx, logsClient, logsQuery, queryContext.TimeRange)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -113,11 +107,11 @@ func (e *cloudWatchExecutor) syncQuery(ctx context.Context, logsClient cloudwatc
|
||||
|
||||
attemptCount := 1
|
||||
for range ticker.C {
|
||||
res, err := e.executeGetQueryResults(ctx, logsClient, requestParams)
|
||||
res, err := ds.executeGetQueryResults(ctx, logsClient, requestParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isTerminated(*res.Status) {
|
||||
if isTerminated(res.Status) {
|
||||
return res, err
|
||||
}
|
||||
if time.Duration(attemptCount)*time.Second >= logsTimeout {
|
||||
|
||||
@@ -7,15 +7,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
|
||||
cloudwatchlogstypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
|
||||
@@ -31,19 +27,15 @@ func Test_executeSyncLogQuery(t *testing.T) {
|
||||
})
|
||||
|
||||
var cli fakeCWLogsClient
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
NewCWLogsClient = func(aws.Config) models.CWLogsClient {
|
||||
return &cli
|
||||
}
|
||||
|
||||
t.Run("getCWLogsClient is called with region from input JSON", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}}
|
||||
sess := fakeSessionCache{}
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &sess}, nil
|
||||
})
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"}}
|
||||
ds := newTestDatasource()
|
||||
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
Headers: map[string]string{headerFromAlert: "some value"},
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
@@ -58,18 +50,15 @@ func Test_executeSyncLogQuery(t *testing.T) {
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"some region"}, sess.calledRegions)
|
||||
//assert.Equal(t, []string{"some region"}, sess.calledRegions)
|
||||
})
|
||||
|
||||
t.Run("getCWLogsClient is called with region from instance manager when region is default", func(t *testing.T) {
|
||||
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}}
|
||||
sess := fakeSessionCache{}
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "instance manager's region"}}, sessions: &sess}, nil
|
||||
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"}}
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.Region = "instance manager's region"
|
||||
})
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
Headers: map[string]string{headerFromAlert: "some value"},
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
@@ -84,7 +73,7 @@ func Test_executeSyncLogQuery(t *testing.T) {
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"instance manager's region"}, sess.calledRegions)
|
||||
//assert.Equal(t, []string{"instance manager's region"}, sess.calledRegions)
|
||||
})
|
||||
|
||||
t.Run("with header", func(t *testing.T) {
|
||||
@@ -111,7 +100,7 @@ func Test_executeSyncLogQuery(t *testing.T) {
|
||||
}
|
||||
origExecuteSyncLogQuery := executeSyncLogQuery
|
||||
var syncCalled bool
|
||||
executeSyncLogQuery = func(ctx context.Context, e *cloudWatchExecutor, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
executeSyncLogQuery = func(ctx context.Context, e *DataSource, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
syncCalled = true
|
||||
return nil, nil
|
||||
}
|
||||
@@ -119,13 +108,11 @@ func Test_executeSyncLogQuery(t *testing.T) {
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
syncCalled = false
|
||||
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}}
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "instance manager's region"}}, sessions: &fakeSessionCache{}}, nil
|
||||
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"}}
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.Region = "instance manager's region"
|
||||
})
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
Headers: tc.headers,
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
@@ -153,7 +140,7 @@ func Test_executeSyncLogQuery(t *testing.T) {
|
||||
t.Run("when query mode is 'Logs' and does not include type or subtype", func(t *testing.T) {
|
||||
origExecuteSyncLogQuery := executeSyncLogQuery
|
||||
syncCalled := false
|
||||
executeSyncLogQuery = func(ctx context.Context, e *cloudWatchExecutor, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
executeSyncLogQuery = func(ctx context.Context, e *DataSource, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
syncCalled = true
|
||||
return nil, nil
|
||||
}
|
||||
@@ -161,13 +148,12 @@ func Test_executeSyncLogQuery(t *testing.T) {
|
||||
executeSyncLogQuery = origExecuteSyncLogQuery
|
||||
})
|
||||
|
||||
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}}
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "instance manager's region"}}, sessions: &fakeSessionCache{}}, nil
|
||||
cli = fakeCWLogsClient{queryResults: cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"}}
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.Region = "instance manager's region"
|
||||
})
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
{
|
||||
@@ -192,22 +178,19 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) {
|
||||
})
|
||||
|
||||
var cli *mockLogsSyncClient
|
||||
NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatchLogsAPI {
|
||||
NewCWLogsClient = func(aws.Config) models.CWLogsClient {
|
||||
return cli
|
||||
}
|
||||
|
||||
t.Run("when a query refId is not provided, 'A' is assigned by default", func(t *testing.T) {
|
||||
cli = &mockLogsSyncClient{}
|
||||
cli.On("StartQueryWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
|
||||
cli.On("StartQuery", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
|
||||
QueryId: aws.String("abcd-efgh-ijkl-mnop"),
|
||||
}, nil)
|
||||
cli.On("GetQueryResultsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}, nil)
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
cli.On("GetQueryResults", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"}, nil)
|
||||
ds := newTestDatasource()
|
||||
|
||||
res, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
res, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
Headers: map[string]string{headerFromAlert: "some value"},
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
@@ -227,16 +210,13 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) {
|
||||
|
||||
t.Run("when a query refId is provided, it is returned in the response", func(t *testing.T) {
|
||||
cli = &mockLogsSyncClient{}
|
||||
cli.On("StartQueryWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
|
||||
cli.On("StartQuery", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
|
||||
QueryId: aws.String("abcd-efgh-ijkl-mnop"),
|
||||
}, nil)
|
||||
cli.On("GetQueryResultsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")}, nil)
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
cli.On("GetQueryResults", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"}, nil)
|
||||
ds := newTestDatasource()
|
||||
|
||||
res, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
res, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
Headers: map[string]string{headerFromAlert: "some value"},
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
@@ -269,43 +249,40 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) {
|
||||
// when each query has a different response from AWS API calls, the RefIds are correctly reassigned to the associated response.
|
||||
cli = &mockLogsSyncClient{}
|
||||
// mock.MatchedBy makes sure that the QueryId below will only be returned when the input expression = "query string for A"
|
||||
cli.On("StartQueryWithContext", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.StartQueryInput) bool {
|
||||
cli.On("StartQuery", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.StartQueryInput) bool {
|
||||
return *input.QueryString == "fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|query string for A"
|
||||
}), mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
|
||||
QueryId: aws.String("queryId for A"),
|
||||
}, nil)
|
||||
|
||||
// mock.MatchedBy makes sure that the QueryId below will only be returned when the input expression = "query string for B"
|
||||
cli.On("StartQueryWithContext", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.StartQueryInput) bool {
|
||||
cli.On("StartQuery", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.StartQueryInput) bool {
|
||||
return *input.QueryString == "fields @timestamp,ltrim(@log) as __log__grafana_internal__,ltrim(@logStream) as __logstream__grafana_internal__|query string for B"
|
||||
}), mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
|
||||
QueryId: aws.String("queryId for B"),
|
||||
}, nil)
|
||||
cli.On("GetQueryResultsWithContext", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.GetQueryResultsInput) bool {
|
||||
cli.On("GetQueryResults", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.GetQueryResultsInput) bool {
|
||||
return *input.QueryId == "queryId for A"
|
||||
}), mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{
|
||||
// this result will only be returned when the argument is QueryId = "queryId for A"
|
||||
Results: [][]*cloudwatchlogs.ResultField{{{
|
||||
Results: [][]cloudwatchlogstypes.ResultField{{{
|
||||
Field: utils.Pointer("@log"),
|
||||
Value: utils.Pointer("A result"),
|
||||
}}},
|
||||
Status: aws.String("Complete")}, nil)
|
||||
cli.On("GetQueryResultsWithContext", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.GetQueryResultsInput) bool {
|
||||
Status: "Complete"}, nil)
|
||||
cli.On("GetQueryResults", mock.Anything, mock.MatchedBy(func(input *cloudwatchlogs.GetQueryResultsInput) bool {
|
||||
return *input.QueryId == "queryId for B"
|
||||
}), mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{
|
||||
// this result will only be returned when the argument is QueryId = "queryId for B"
|
||||
Results: [][]*cloudwatchlogs.ResultField{{{
|
||||
Results: [][]cloudwatchlogstypes.ResultField{{{
|
||||
Field: utils.Pointer("@log"),
|
||||
Value: utils.Pointer("B result"),
|
||||
}}},
|
||||
Status: aws.String("Complete")}, nil)
|
||||
Status: "Complete"}, nil)
|
||||
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource()
|
||||
|
||||
res, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
res, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
Headers: map[string]string{headerFromAlert: "some value"},
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
@@ -328,30 +305,29 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
expectedLogFieldFromFirstCall := data.NewField("@log", nil, []*string{utils.Pointer("A result")}) // verifies the response from GetQueryResultsWithContext matches the input RefId A
|
||||
expectedLogFieldFromFirstCall := data.NewField("@log", nil, []*string{utils.Pointer("A result")}) // verifies the response from GetQueryResults matches the input RefId A
|
||||
assert.NoError(t, err)
|
||||
respA, ok := res.Responses["A"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []*data.Field{expectedLogFieldFromFirstCall}, respA.Frames[0].Fields)
|
||||
|
||||
expectedLogFieldFromSecondCall := data.NewField("@log", nil, []*string{utils.Pointer("B result")}) // verifies the response from GetQueryResultsWithContext matches the input RefId B
|
||||
expectedLogFieldFromSecondCall := data.NewField("@log", nil, []*string{utils.Pointer("B result")}) // verifies the response from GetQueryResults matches the input RefId B
|
||||
respB, ok := res.Responses["B"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []*data.Field{expectedLogFieldFromSecondCall}, respB.Frames[0].Fields)
|
||||
})
|
||||
t.Run("when logsTimeout setting is defined, the polling period will be set to that variable", func(t *testing.T) {
|
||||
cli = &mockLogsSyncClient{}
|
||||
cli.On("StartQueryWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
|
||||
cli.On("StartQuery", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
|
||||
QueryId: aws.String("abcd-efgh-ijkl-mnop"),
|
||||
}, nil)
|
||||
cli.On("GetQueryResultsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Running")}, nil)
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{LogsTimeout: models.Duration{Duration: time.Millisecond}}, sessions: &fakeSessionCache{}}, nil
|
||||
cli.On("GetQueryResults", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.GetQueryResultsOutput{Status: "Running"}, nil)
|
||||
|
||||
ds := newTestDatasource(func(ds *DataSource) {
|
||||
ds.Settings.LogsTimeout = models.Duration{Duration: time.Millisecond}
|
||||
})
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
|
||||
_, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
_, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
Headers: map[string]string{headerFromAlert: "some value"},
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
@@ -366,24 +342,21 @@ func Test_executeSyncLogQuery_handles_RefId_from_input_queries(t *testing.T) {
|
||||
},
|
||||
})
|
||||
assert.Error(t, err)
|
||||
cli.AssertNumberOfCalls(t, "GetQueryResultsWithContext", 1)
|
||||
cli.AssertNumberOfCalls(t, "GetQueryResults", 1)
|
||||
})
|
||||
|
||||
t.Run("when getQueryResults returns aws error is returned, it keeps the context", func(t *testing.T) {
|
||||
cli = &mockLogsSyncClient{}
|
||||
cli.On("StartQueryWithContext", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
|
||||
cli.On("StartQuery", mock.Anything, mock.Anything, mock.Anything).Return(&cloudwatchlogs.StartQueryOutput{
|
||||
QueryId: aws.String("abcd-efgh-ijkl-mnop"),
|
||||
}, nil)
|
||||
cli.On("GetQueryResultsWithContext", mock.Anything, mock.Anything, mock.Anything).Return(
|
||||
&cloudwatchlogs.GetQueryResultsOutput{Status: aws.String("Complete")},
|
||||
&fakeAWSError{code: "foo", message: "bar"},
|
||||
cli.On("GetQueryResults", mock.Anything, mock.Anything, mock.Anything).Return(
|
||||
&cloudwatchlogs.GetQueryResultsOutput{Status: "Complete"},
|
||||
&fakeSmithyError{code: "foo", message: "bar"},
|
||||
)
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource()
|
||||
|
||||
res, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
res, err := ds.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||
Headers: map[string]string{headerFromAlert: "some value"},
|
||||
PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}},
|
||||
Queries: []backend.DataQuery{
|
||||
|
||||
@@ -4,30 +4,31 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
|
||||
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
)
|
||||
|
||||
func (e *cloudWatchExecutor) buildMetricDataInput(ctx context.Context, startTime time.Time, endTime time.Time,
|
||||
func (ds *DataSource) buildMetricDataInput(ctx context.Context, startTime time.Time, endTime time.Time,
|
||||
queries []*models.CloudWatchQuery) (*cloudwatch.GetMetricDataInput, error) {
|
||||
metricDataInput := &cloudwatch.GetMetricDataInput{
|
||||
StartTime: aws.Time(startTime),
|
||||
EndTime: aws.Time(endTime),
|
||||
ScanBy: aws.String("TimestampAscending"),
|
||||
ScanBy: cloudwatchtypes.ScanByTimestampAscending,
|
||||
}
|
||||
|
||||
shouldSetLabelOptions := len(queries) > 0 && len(queries[0].TimezoneUTCOffset) > 0
|
||||
|
||||
if shouldSetLabelOptions {
|
||||
metricDataInput.LabelOptions = &cloudwatch.LabelOptions{
|
||||
metricDataInput.LabelOptions = &cloudwatchtypes.LabelOptions{
|
||||
Timezone: aws.String(queries[0].TimezoneUTCOffset),
|
||||
}
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
metricDataQuery, err := e.buildMetricDataQuery(ctx, query)
|
||||
metricDataQuery, err := ds.buildMetricDataQuery(ctx, query)
|
||||
if err != nil {
|
||||
return nil, &models.QueryError{Err: err, RefID: query.RefId}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
)
|
||||
|
||||
@@ -20,21 +20,21 @@ func TestMetricDataInputBuilder(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
timezoneUTCOffset string
|
||||
expectedLabelOptions *cloudwatch.LabelOptions
|
||||
expectedLabelOptions *cloudwatchtypes.LabelOptions
|
||||
}{
|
||||
{name: "when timezoneUTCOffset is provided", timezoneUTCOffset: "+1234", expectedLabelOptions: &cloudwatch.LabelOptions{Timezone: aws.String("+1234")}},
|
||||
{name: "when timezoneUTCOffset is provided", timezoneUTCOffset: "+1234", expectedLabelOptions: &cloudwatchtypes.LabelOptions{Timezone: aws.String("+1234")}},
|
||||
{name: "when timezoneUTCOffset is not provided", timezoneUTCOffset: "", expectedLabelOptions: nil},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
executor := newExecutor(nil, log.NewNullLogger())
|
||||
ds := newTestDatasource()
|
||||
query := getBaseQuery()
|
||||
query.TimezoneUTCOffset = tc.timezoneUTCOffset
|
||||
|
||||
from := now.Add(time.Hour * -2)
|
||||
to := now.Add(time.Hour * -1)
|
||||
mdi, err := executor.buildMetricDataInput(context.Background(), from, to, []*models.CloudWatchQuery{query})
|
||||
mdi, err := ds.buildMetricDataInput(context.Background(), from, to, []*models.CloudWatchQuery{query})
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, mdi)
|
||||
|
||||
@@ -4,11 +4,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
@@ -16,8 +15,8 @@ import (
|
||||
|
||||
const keySeparator = "|&|"
|
||||
|
||||
func (e *cloudWatchExecutor) buildMetricDataQuery(ctx context.Context, query *models.CloudWatchQuery) (*cloudwatch.MetricDataQuery, error) {
|
||||
mdq := &cloudwatch.MetricDataQuery{
|
||||
func (ds *DataSource) buildMetricDataQuery(ctx context.Context, query *models.CloudWatchQuery) (cloudwatchtypes.MetricDataQuery, error) {
|
||||
mdq := cloudwatchtypes.MetricDataQuery{
|
||||
Id: aws.String(query.Id),
|
||||
ReturnData: aws.Bool(query.ReturnData),
|
||||
}
|
||||
@@ -28,10 +27,10 @@ func (e *cloudWatchExecutor) buildMetricDataQuery(ctx context.Context, query *mo
|
||||
|
||||
switch query.GetGetMetricDataAPIMode() {
|
||||
case models.GMDApiModeMathExpression:
|
||||
mdq.Period = aws.Int64(int64(query.Period))
|
||||
mdq.Period = &query.Period
|
||||
mdq.Expression = aws.String(query.Expression)
|
||||
case models.GMDApiModeSQLExpression:
|
||||
mdq.Period = aws.Int64(int64(query.Period))
|
||||
mdq.Period = &query.Period
|
||||
mdq.Expression = aws.String(query.SqlExpression)
|
||||
case models.GMDApiModeInferredSearchExpression:
|
||||
mdq.Expression = aws.String(buildSearchExpression(query, query.Statistic))
|
||||
@@ -39,17 +38,17 @@ func (e *cloudWatchExecutor) buildMetricDataQuery(ctx context.Context, query *mo
|
||||
mdq.Label = aws.String(buildSearchExpressionLabel(query))
|
||||
}
|
||||
case models.GMDApiModeMetricStat:
|
||||
mdq.MetricStat = &cloudwatch.MetricStat{
|
||||
Metric: &cloudwatch.Metric{
|
||||
mdq.MetricStat = &cloudwatchtypes.MetricStat{
|
||||
Metric: &cloudwatchtypes.Metric{
|
||||
Namespace: aws.String(query.Namespace),
|
||||
MetricName: aws.String(query.MetricName),
|
||||
Dimensions: make([]*cloudwatch.Dimension, 0),
|
||||
Dimensions: make([]cloudwatchtypes.Dimension, 0),
|
||||
},
|
||||
Period: aws.Int64(int64(query.Period)),
|
||||
Period: &query.Period,
|
||||
}
|
||||
for key, values := range query.Dimensions {
|
||||
mdq.MetricStat.Metric.Dimensions = append(mdq.MetricStat.Metric.Dimensions,
|
||||
&cloudwatch.Dimension{
|
||||
cloudwatchtypes.Dimension{
|
||||
Name: aws.String(key),
|
||||
Value: aws.String(values[0]),
|
||||
})
|
||||
@@ -121,14 +120,14 @@ func buildSearchExpression(query *models.CloudWatchQuery, stat string) string {
|
||||
}
|
||||
schema = fmt.Sprintf("{%s}", schema)
|
||||
schemaSearchTermAndAccount := strings.TrimSpace(strings.Join([]string{schema, searchTerm, account}, " "))
|
||||
return fmt.Sprintf("REMOVE_EMPTY(SEARCH('%s', '%s', %s))", schemaSearchTermAndAccount, stat, strconv.Itoa(query.Period))
|
||||
return fmt.Sprintf("REMOVE_EMPTY(SEARCH('%s', '%s', %d))", schemaSearchTermAndAccount, stat, query.Period)
|
||||
}
|
||||
|
||||
sort.Strings(dimensionNamesWithoutKnownValues)
|
||||
searchTerm = appendSearch(searchTerm, join(dimensionNamesWithoutKnownValues, " ", `"`, `"`))
|
||||
namespace := fmt.Sprintf("Namespace=%q", query.Namespace)
|
||||
namespaceSearchTermAndAccount := strings.TrimSpace(strings.Join([]string{namespace, searchTerm, account}, " "))
|
||||
return fmt.Sprintf(`REMOVE_EMPTY(SEARCH('%s', '%s', %s))`, namespaceSearchTermAndAccount, stat, strconv.Itoa(query.Period))
|
||||
return fmt.Sprintf(`REMOVE_EMPTY(SEARCH('%s', '%s', %d))`, namespaceSearchTermAndAccount, stat, query.Period)
|
||||
}
|
||||
|
||||
func buildSearchExpressionLabel(query *models.CloudWatchQuery) string {
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/features"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -13,13 +13,13 @@ import (
|
||||
)
|
||||
|
||||
func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
executor := newExecutor(nil, log.NewNullLogger())
|
||||
ds := newTestDatasource()
|
||||
t.Run("buildMetricDataQuery", func(t *testing.T) {
|
||||
t.Run("should use metric stat", func(t *testing.T) {
|
||||
query := getBaseQuery()
|
||||
query.MetricEditorMode = models.MetricEditorModeBuilder
|
||||
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
|
||||
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, mdq.Expression)
|
||||
assert.Equal(t, query.MetricName, *mdq.MetricStat.Metric.MetricName)
|
||||
@@ -31,7 +31,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
query.MetricEditorMode = models.MetricEditorModeBuilder
|
||||
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||
query.AccountId = aws.String("some account id")
|
||||
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
|
||||
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "some account id", *mdq.AccountId)
|
||||
})
|
||||
@@ -40,7 +40,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
query := getBaseQuery()
|
||||
query.MetricEditorMode = models.MetricEditorModeBuilder
|
||||
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
|
||||
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, mdq.AccountId)
|
||||
})
|
||||
@@ -50,7 +50,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
query.MetricEditorMode = models.MetricEditorModeBuilder
|
||||
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||
query.MatchExact = false
|
||||
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
|
||||
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, mdq.MetricStat)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"="lb1"', '', 300))`, *mdq.Expression)
|
||||
@@ -61,7 +61,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
query.MetricEditorMode = models.MetricEditorModeRaw
|
||||
query.MetricQueryType = models.MetricQueryTypeQuery
|
||||
query.SqlExpression = `SELECT SUM(CPUUTilization) FROM "AWS/EC2"`
|
||||
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
|
||||
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, mdq.MetricStat)
|
||||
assert.Equal(t, query.SqlExpression, *mdq.Expression)
|
||||
@@ -72,32 +72,30 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
query.MetricEditorMode = models.MetricEditorModeRaw
|
||||
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||
query.Expression = `SUM(x+y)`
|
||||
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
|
||||
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, mdq.MetricStat)
|
||||
assert.Equal(t, query.Expression, *mdq.Expression)
|
||||
})
|
||||
|
||||
t.Run("should set period in user defined expression", func(t *testing.T) {
|
||||
executor := newExecutor(nil, log.NewNullLogger())
|
||||
query := getBaseQuery()
|
||||
query.MetricEditorMode = models.MetricEditorModeRaw
|
||||
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||
query.MatchExact = false
|
||||
query.Expression = `SUM([a,b])`
|
||||
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
|
||||
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, mdq.MetricStat)
|
||||
assert.Equal(t, int64(300), *mdq.Period)
|
||||
assert.Equal(t, int32(300), *mdq.Period)
|
||||
assert.Equal(t, `SUM([a,b])`, *mdq.Expression)
|
||||
})
|
||||
|
||||
t.Run("should set label", func(t *testing.T) {
|
||||
executor := newExecutor(nil, log.NewNullLogger())
|
||||
query := getBaseQuery()
|
||||
query.Label = "some label"
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
|
||||
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, mdq.Label)
|
||||
@@ -105,18 +103,16 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should not set label for empty string query label", func(t *testing.T) {
|
||||
executor := newExecutor(nil, log.NewNullLogger())
|
||||
query := getBaseQuery()
|
||||
query.Label = ""
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
|
||||
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, mdq.Label)
|
||||
})
|
||||
|
||||
t.Run(`should not specify accountId when it is "all"`, func(t *testing.T) {
|
||||
executor := newExecutor(nil, log.NewNullLogger())
|
||||
query := &models.CloudWatchQuery{
|
||||
Namespace: "AWS/EC2",
|
||||
MetricName: "CPUUtilization",
|
||||
@@ -126,7 +122,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
AccountId: aws.String("all"),
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
|
||||
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.Nil(t, mdq.MetricStat)
|
||||
@@ -134,7 +130,6 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should set accountId when it is specified", func(t *testing.T) {
|
||||
executor := newExecutor(nil, log.NewNullLogger())
|
||||
query := &models.CloudWatchQuery{
|
||||
Namespace: "AWS/EC2",
|
||||
MetricName: "CPUUtilization",
|
||||
@@ -144,7 +139,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
AccountId: aws.String("12345"),
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(context.Background(), query)
|
||||
mdq, err := ds.buildMetricDataQuery(context.Background(), query)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.Nil(t, mdq.MetricStat)
|
||||
@@ -170,7 +165,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","LoadBalancer"} MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "${LABEL}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
|
||||
@@ -192,7 +187,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","InstanceId","LoadBalancer"} MetricName="CPUUtilization" "InstanceId"=("i-123" OR "i-456" OR "i-789") "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
|
||||
@@ -213,7 +208,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","LoadBalancer"} MetricName="CPUUtilization"', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "${LABEL}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
|
||||
@@ -235,7 +230,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","InstanceId","LoadBalancer"} MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
|
||||
@@ -258,7 +253,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/EC2","InstanceId","LoadBalancer"} MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3") :aws.AccountId="some account id"', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
|
||||
@@ -279,7 +274,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"AWS/Kafka","Cluster Name"} MetricName="CpuUser" "Cluster Name"=("dev-cluster" OR "prod-cluster")', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "${LABEL}|&|${PROP('Dim.Cluster Name')}", *mdq.Label)
|
||||
@@ -301,7 +296,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"Test-API Cache by Minute","InstanceId","LoadBalancer"} MetricName="CpuUser" "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
|
||||
@@ -324,7 +319,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('{"CPUUtilization","InstanceId","LoadBalancer"} MetricName="CpuUser" "LoadBalancer"="lb1"', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "LB: ${PROP('Dim.LoadBalancer')|&|${PROP('Dim.InstanceId')}", *mdq.Label)
|
||||
@@ -349,7 +344,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "${LABEL}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
|
||||
@@ -371,7 +366,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "InstanceId"=("i-123" OR "i-456" OR "i-789") "LoadBalancer"=("lb1" OR "lb2" OR "lb3")', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
|
||||
@@ -392,7 +387,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "${LABEL}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
|
||||
@@ -414,7 +409,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3") "InstanceId"', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
|
||||
@@ -437,7 +432,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"=("lb1" OR "lb2" OR "lb3") "InstanceId" :aws.AccountId="some account id"', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "${LABEL}|&|${PROP('Dim.InstanceId')}|&|${PROP('Dim.LoadBalancer')}", *mdq.Label)
|
||||
@@ -460,7 +455,7 @@ func TestMetricDataQueryBuilder(t *testing.T) {
|
||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||
}
|
||||
|
||||
mdq, err := executor.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
mdq, err := ds.buildMetricDataQuery(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), query)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `REMOVE_EMPTY(SEARCH('Namespace="AWS/EC2" MetricName="CPUUtilization" "LoadBalancer"="lb1" "InstanceId"', 'Average', 300))`, *mdq.Expression)
|
||||
assert.Equal(t, "LB: ${PROP('Dim.LoadBalancer')|&|${PROP('Dim.InstanceId')}", *mdq.Label)
|
||||
|
||||
@@ -12,11 +12,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
|
||||
resourcegroupstaggingapitypes "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
|
||||
)
|
||||
|
||||
type suggestData struct {
|
||||
@@ -39,12 +41,12 @@ func parseMultiSelectValue(input string) []string {
|
||||
return []string{trimmedInput}
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) handleGetEbsVolumeIds(ctx context.Context, pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) {
|
||||
func (ds *DataSource) handleGetEbsVolumeIds(ctx context.Context, parameters url.Values) ([]suggestData, error) {
|
||||
region := parameters.Get("region")
|
||||
instanceId := parameters.Get("instanceId")
|
||||
|
||||
instanceIds := aws.StringSlice(parseMultiSelectValue(instanceId))
|
||||
instances, err := e.ec2DescribeInstances(ctx, pluginCtx, region, nil, instanceIds)
|
||||
instanceIds := parseMultiSelectValue(instanceId)
|
||||
instances, err := ds.ec2DescribeInstances(ctx, region, nil, instanceIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -61,7 +63,7 @@ func (e *cloudWatchExecutor) handleGetEbsVolumeIds(ctx context.Context, pluginCt
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context, pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) {
|
||||
func (ds *DataSource) handleGetEc2InstanceAttribute(ctx context.Context, parameters url.Values) ([]suggestData, error) {
|
||||
region := parameters.Get("region")
|
||||
attributeName := parameters.Get("attributeName")
|
||||
filterJson := parameters.Get("filters")
|
||||
@@ -72,23 +74,23 @@ func (e *cloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context,
|
||||
return nil, fmt.Errorf("error unmarshaling filter: %v", err)
|
||||
}
|
||||
|
||||
var filters []*ec2.Filter
|
||||
var filters []ec2types.Filter
|
||||
for k, v := range filterMap {
|
||||
if vv, ok := v.([]any); ok {
|
||||
var values []*string
|
||||
var values []string
|
||||
for _, vvv := range vv {
|
||||
if vvvv, ok := vvv.(string); ok {
|
||||
values = append(values, &vvvv)
|
||||
values = append(values, vvvv)
|
||||
}
|
||||
}
|
||||
filters = append(filters, &ec2.Filter{
|
||||
filters = append(filters, ec2types.Filter{
|
||||
Name: aws.String(k),
|
||||
Values: values,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
instances, err := e.ec2DescribeInstances(ctx, pluginCtx, region, filters, nil)
|
||||
instances, err := ds.ec2DescribeInstances(ctx, region, filters, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -120,7 +122,7 @@ func (e *cloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context,
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getInstanceAttributeValue(attributeName string, instance *ec2.Instance) (value string, found bool, err error) {
|
||||
func getInstanceAttributeValue(attributeName string, instance ec2types.Instance) (value string, found bool, err error) {
|
||||
tags := make(map[string]string)
|
||||
for _, tag := range instance.Tags {
|
||||
tags[*tag.Key] = *tag.Value
|
||||
@@ -152,7 +154,12 @@ func getInstanceAttributeValue(attributeName string, instance *ec2.Instance) (va
|
||||
if v.Kind() == reflect.Ptr && v.IsNil() {
|
||||
return "", false, nil
|
||||
}
|
||||
if attr, ok := v.Interface().(*string); ok {
|
||||
if v.Kind() == reflect.String {
|
||||
if v.String() == "" {
|
||||
return "", false, nil
|
||||
}
|
||||
data = v.String()
|
||||
} else if attr, ok := v.Interface().(*string); ok {
|
||||
data = *attr
|
||||
} else if attr, ok := v.Interface().(*time.Time); ok {
|
||||
data = attr.String()
|
||||
@@ -168,7 +175,7 @@ func getInstanceAttributeValue(attributeName string, instance *ec2.Instance) (va
|
||||
return data, true, nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) handleGetResourceArns(ctx context.Context, pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) {
|
||||
func (ds *DataSource) handleGetResourceArns(ctx context.Context, parameters url.Values) ([]suggestData, error) {
|
||||
region := parameters.Get("region")
|
||||
resourceType := parameters.Get("resourceType")
|
||||
tagsJson := parameters.Get("tags")
|
||||
@@ -179,26 +186,25 @@ func (e *cloudWatchExecutor) handleGetResourceArns(ctx context.Context, pluginCt
|
||||
return nil, fmt.Errorf("error unmarshaling filter: %v", err)
|
||||
}
|
||||
|
||||
var filters []*resourcegroupstaggingapi.TagFilter
|
||||
var filters []resourcegroupstaggingapitypes.TagFilter
|
||||
for k, v := range tagsMap {
|
||||
if vv, ok := v.([]any); ok {
|
||||
var values []*string
|
||||
var values []string
|
||||
for _, vvv := range vv {
|
||||
if vvvv, ok := vvv.(string); ok {
|
||||
values = append(values, &vvvv)
|
||||
values = append(values, vvvv)
|
||||
}
|
||||
}
|
||||
filters = append(filters, &resourcegroupstaggingapi.TagFilter{
|
||||
filters = append(filters, resourcegroupstaggingapitypes.TagFilter{
|
||||
Key: aws.String(k),
|
||||
Values: values,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var resourceTypes []*string
|
||||
resourceTypes = append(resourceTypes, &resourceType)
|
||||
resourceTypes := []string{resourceType}
|
||||
|
||||
resources, err := e.resourceGroupsGetResources(ctx, pluginCtx, region, filters, resourceTypes)
|
||||
resources, err := ds.resourceGroupsGetResources(ctx, region, filters, resourceTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -212,75 +218,77 @@ func (e *cloudWatchExecutor) handleGetResourceArns(ctx context.Context, pluginCt
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) ec2DescribeInstances(ctx context.Context, pluginCtx backend.PluginContext, region string, filters []*ec2.Filter, instanceIds []*string) (*ec2.DescribeInstancesOutput, error) {
|
||||
func (ds *DataSource) ec2DescribeInstances(ctx context.Context, region string, filters []ec2types.Filter, instanceIds []string) (*ec2.DescribeInstancesOutput, error) {
|
||||
params := &ec2.DescribeInstancesInput{
|
||||
Filters: filters,
|
||||
InstanceIds: instanceIds,
|
||||
}
|
||||
|
||||
client, err := e.getEC2Client(ctx, pluginCtx, region)
|
||||
client, err := ds.getEC2Client(ctx, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp ec2.DescribeInstancesOutput
|
||||
if err := client.DescribeInstancesPagesWithContext(ctx, params, func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
|
||||
resp := &ec2.DescribeInstancesOutput{}
|
||||
pager := ec2.NewDescribeInstancesPaginator(client, params)
|
||||
for pager.HasMorePages() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("describe instances pager failed: %w", err)
|
||||
}
|
||||
resp.Reservations = append(resp.Reservations, page.Reservations...)
|
||||
return !lastPage
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to call ec2:DescribeInstances, %w", err)
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (e *cloudWatchExecutor) resourceGroupsGetResources(ctx context.Context, pluginCtx backend.PluginContext, region string, filters []*resourcegroupstaggingapi.TagFilter,
|
||||
resourceTypes []*string) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
|
||||
func (ds *DataSource) resourceGroupsGetResources(ctx context.Context, region string, filters []resourcegroupstaggingapitypes.TagFilter,
|
||||
resourceTypes []string) (*resourcegroupstaggingapi.GetResourcesOutput, error) {
|
||||
params := &resourcegroupstaggingapi.GetResourcesInput{
|
||||
ResourceTypeFilters: resourceTypes,
|
||||
TagFilters: filters,
|
||||
}
|
||||
|
||||
client, err := e.getRGTAClient(ctx, pluginCtx, region)
|
||||
client, err := ds.getRGTAClient(ctx, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp resourcegroupstaggingapi.GetResourcesOutput
|
||||
if err := client.GetResourcesPagesWithContext(ctx, params,
|
||||
func(page *resourcegroupstaggingapi.GetResourcesOutput, lastPage bool) bool {
|
||||
resp.ResourceTagMappingList = append(resp.ResourceTagMappingList, page.ResourceTagMappingList...)
|
||||
return !lastPage
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to call tag:GetResources, %w", err)
|
||||
paginator := resourcegroupstaggingapi.NewGetResourcesPaginator(client, params)
|
||||
for paginator.HasMorePages() {
|
||||
page, err := paginator.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get resource groups paginator failed: %w", err)
|
||||
}
|
||||
resp.ResourceTagMappingList = append(resp.ResourceTagMappingList, page.ResourceTagMappingList...)
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// legacy route, will be removed once GovCloud supports Cross Account Observability
|
||||
func (e *cloudWatchExecutor) handleGetLogGroups(ctx context.Context, pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) {
|
||||
func (ds *DataSource) handleGetLogGroups(ctx context.Context, parameters url.Values) ([]suggestData, error) {
|
||||
region := parameters.Get("region")
|
||||
limit := parameters.Get("limit")
|
||||
logGroupNamePrefix := parameters.Get("logGroupNamePrefix")
|
||||
|
||||
logsClient, err := e.getCWLogsClient(ctx, pluginCtx, region)
|
||||
logsClient, err := ds.getCWLogsClient(ctx, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logGroupLimit := defaultLogGroupLimit
|
||||
intLimit, err := strconv.ParseInt(limit, 10, 64)
|
||||
intLimit, err := strconv.ParseInt(limit, 10, 32)
|
||||
if err == nil && intLimit > 0 {
|
||||
logGroupLimit = intLimit
|
||||
logGroupLimit = int32(intLimit)
|
||||
}
|
||||
|
||||
input := &cloudwatchlogs.DescribeLogGroupsInput{Limit: aws.Int64(logGroupLimit)}
|
||||
input := &cloudwatchlogs.DescribeLogGroupsInput{Limit: aws.Int32(logGroupLimit)}
|
||||
if len(logGroupNamePrefix) > 0 {
|
||||
input.LogGroupNamePrefix = aws.String(logGroupNamePrefix)
|
||||
}
|
||||
var response *cloudwatchlogs.DescribeLogGroupsOutput
|
||||
response, err = logsClient.DescribeLogGroupsWithContext(ctx, input)
|
||||
response, err = logsClient.DescribeLogGroups(ctx, input)
|
||||
if err != nil || response == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -6,41 +6,37 @@ import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/client"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
|
||||
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
|
||||
resourcegroupstaggingapitypes "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQuery_InstanceAttributes(t *testing.T) {
|
||||
origNewEC2Client := NewEC2Client
|
||||
origNewEC2API := NewEC2API
|
||||
t.Cleanup(func() {
|
||||
NewEC2Client = origNewEC2Client
|
||||
NewEC2API = origNewEC2API
|
||||
})
|
||||
|
||||
var cli oldEC2Client
|
||||
|
||||
NewEC2Client = func(client.ConfigProvider) models.EC2APIProvider {
|
||||
NewEC2API = func(aws.Config) models.EC2APIProvider {
|
||||
return cli
|
||||
}
|
||||
|
||||
t.Run("Get instance ID", func(t *testing.T) {
|
||||
const instanceID = "i-12345678"
|
||||
cli = oldEC2Client{
|
||||
reservations: []*ec2.Reservation{
|
||||
reservations: []ec2types.Reservation{
|
||||
{
|
||||
Instances: []*ec2.Instance{
|
||||
Instances: []ec2types.Instance{
|
||||
{
|
||||
InstanceId: aws.String(instanceID),
|
||||
Tags: []*ec2.Tag{
|
||||
Tags: []ec2types.Tag{
|
||||
{
|
||||
Key: aws.String("Environment"),
|
||||
Value: aws.String("production"),
|
||||
@@ -52,22 +48,16 @@ func TestQuery_InstanceAttributes(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
|
||||
filterMap := map[string][]string{
|
||||
"tag:Environment": {"production"},
|
||||
}
|
||||
filterJson, err := json.Marshal(filterMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
resp, err := executor.handleGetEc2InstanceAttribute(
|
||||
ds := newTestDatasource()
|
||||
resp, err := ds.handleGetEc2InstanceAttribute(
|
||||
context.Background(),
|
||||
backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
|
||||
}, url.Values{
|
||||
url.Values{
|
||||
"region": []string{"us-east-1"},
|
||||
"attributeName": []string{"InstanceId"},
|
||||
"filters": []string{string(filterJson)},
|
||||
@@ -82,17 +72,17 @@ func TestQuery_InstanceAttributes(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Get different types", func(t *testing.T) {
|
||||
var expectedInt int64 = 3
|
||||
var expectedInt int32 = 3
|
||||
var expectedBool = true
|
||||
var expectedArn = "arn"
|
||||
cli = oldEC2Client{
|
||||
reservations: []*ec2.Reservation{
|
||||
reservations: []ec2types.Reservation{
|
||||
{
|
||||
Instances: []*ec2.Instance{
|
||||
Instances: []ec2types.Instance{
|
||||
{
|
||||
AmiLaunchIndex: &expectedInt,
|
||||
EbsOptimized: &expectedBool,
|
||||
IamInstanceProfile: &ec2.IamInstanceProfile{
|
||||
IamInstanceProfile: &ec2types.IamInstanceProfile{
|
||||
Arn: &expectedArn,
|
||||
},
|
||||
},
|
||||
@@ -101,11 +91,7 @@ func TestQuery_InstanceAttributes(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
ds := newTestDatasource()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
@@ -145,11 +131,9 @@ func TestQuery_InstanceAttributes(t *testing.T) {
|
||||
filterJson, err := json.Marshal(filterMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := executor.handleGetEc2InstanceAttribute(
|
||||
resp, err := ds.handleGetEc2InstanceAttribute(
|
||||
context.Background(),
|
||||
backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
|
||||
}, url.Values{
|
||||
url.Values{
|
||||
"region": []string{"us-east-1"},
|
||||
"attributeName": []string{tc.attributeName},
|
||||
"filters": []string{string(filterJson)},
|
||||
@@ -163,52 +147,52 @@ func TestQuery_InstanceAttributes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestQuery_EBSVolumeIDs(t *testing.T) {
|
||||
origNewEC2Client := NewEC2Client
|
||||
origNewEC2API := NewEC2API
|
||||
t.Cleanup(func() {
|
||||
NewEC2Client = origNewEC2Client
|
||||
NewEC2API = origNewEC2API
|
||||
})
|
||||
|
||||
var cli oldEC2Client
|
||||
|
||||
NewEC2Client = func(client.ConfigProvider) models.EC2APIProvider {
|
||||
NewEC2API = func(aws.Config) models.EC2APIProvider {
|
||||
return cli
|
||||
}
|
||||
|
||||
t.Run("", func(t *testing.T) {
|
||||
cli = oldEC2Client{
|
||||
reservations: []*ec2.Reservation{
|
||||
reservations: []ec2types.Reservation{
|
||||
{
|
||||
Instances: []*ec2.Instance{
|
||||
Instances: []ec2types.Instance{
|
||||
{
|
||||
InstanceId: aws.String("i-1"),
|
||||
BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{
|
||||
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-1-1")}},
|
||||
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-1-2")}},
|
||||
BlockDeviceMappings: []ec2types.InstanceBlockDeviceMapping{
|
||||
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-1-1")}},
|
||||
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-1-2")}},
|
||||
},
|
||||
},
|
||||
{
|
||||
InstanceId: aws.String("i-2"),
|
||||
BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{
|
||||
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-2-1")}},
|
||||
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-2-2")}},
|
||||
BlockDeviceMappings: []ec2types.InstanceBlockDeviceMapping{
|
||||
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-2-1")}},
|
||||
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-2-2")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Instances: []*ec2.Instance{
|
||||
Instances: []ec2types.Instance{
|
||||
{
|
||||
InstanceId: aws.String("i-3"),
|
||||
BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{
|
||||
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-3-1")}},
|
||||
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-3-2")}},
|
||||
BlockDeviceMappings: []ec2types.InstanceBlockDeviceMapping{
|
||||
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-3-1")}},
|
||||
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-3-2")}},
|
||||
},
|
||||
},
|
||||
{
|
||||
InstanceId: aws.String("i-4"),
|
||||
BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{
|
||||
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-4-1")}},
|
||||
{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-4-2")}},
|
||||
BlockDeviceMappings: []ec2types.InstanceBlockDeviceMapping{
|
||||
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-4-1")}},
|
||||
{Ebs: &ec2types.EbsInstanceBlockDevice{VolumeId: aws.String("vol-4-2")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -216,16 +200,10 @@ func TestQuery_EBSVolumeIDs(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
resp, err := executor.handleGetEbsVolumeIds(
|
||||
ds := newTestDatasource()
|
||||
resp, err := ds.handleGetEbsVolumeIds(
|
||||
context.Background(),
|
||||
backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
|
||||
}, url.Values{
|
||||
url.Values{
|
||||
"region": []string{"us-east-1"},
|
||||
"instanceId": []string{"{i-1, i-2, i-3}"},
|
||||
},
|
||||
@@ -242,23 +220,23 @@ func TestQuery_EBSVolumeIDs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestQuery_ResourceARNs(t *testing.T) {
|
||||
origNewRGTAClient := newRGTAClient
|
||||
origNewRGTAClient := NewRGTAClient
|
||||
t.Cleanup(func() {
|
||||
newRGTAClient = origNewRGTAClient
|
||||
NewRGTAClient = origNewRGTAClient
|
||||
})
|
||||
|
||||
var cli fakeRGTAClient
|
||||
|
||||
newRGTAClient = func(client.ConfigProvider) resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI {
|
||||
NewRGTAClient = func(aws.Config) resourcegroupstaggingapi.GetResourcesAPIClient {
|
||||
return cli
|
||||
}
|
||||
|
||||
t.Run("", func(t *testing.T) {
|
||||
cli = fakeRGTAClient{
|
||||
tagMapping: []*resourcegroupstaggingapi.ResourceTagMapping{
|
||||
tagMapping: []resourcegroupstaggingapitypes.ResourceTagMapping{
|
||||
{
|
||||
ResourceARN: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567"),
|
||||
Tags: []*resourcegroupstaggingapi.Tag{
|
||||
Tags: []resourcegroupstaggingapitypes.Tag{
|
||||
{
|
||||
Key: aws.String("Environment"),
|
||||
Value: aws.String("production"),
|
||||
@@ -267,7 +245,7 @@ func TestQuery_ResourceARNs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
ResourceARN: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321"),
|
||||
Tags: []*resourcegroupstaggingapi.Tag{
|
||||
Tags: []resourcegroupstaggingapitypes.Tag{
|
||||
{
|
||||
Key: aws.String("Environment"),
|
||||
Value: aws.String("production"),
|
||||
@@ -277,22 +255,16 @@ func TestQuery_ResourceARNs(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return DataSource{Settings: models.CloudWatchSettings{}, sessions: &fakeSessionCache{}}, nil
|
||||
})
|
||||
|
||||
tagMap := map[string][]string{
|
||||
"Environment": {"production"},
|
||||
}
|
||||
tagJson, err := json.Marshal(tagMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
executor := newExecutor(im, log.NewNullLogger())
|
||||
resp, err := executor.handleGetResourceArns(
|
||||
ds := newTestDatasource()
|
||||
resp, err := ds.handleGetResourceArns(
|
||||
context.Background(),
|
||||
backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{},
|
||||
}, url.Values{
|
||||
url.Values{
|
||||
"region": []string{"us-east-1"},
|
||||
"resourceType": []string{"ec2:instance"},
|
||||
"tags": []string{string(tagJson)},
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package routes
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
|
||||
@@ -17,15 +15,21 @@ import (
|
||||
)
|
||||
|
||||
func Test_Metrics_Route(t *testing.T) {
|
||||
origNewListMetricsServices := services.NewListMetricsService
|
||||
t.Cleanup(func() {
|
||||
services.NewListMetricsService = origNewListMetricsServices
|
||||
})
|
||||
var mockListMetricsService mocks.ListMetricsServiceMock
|
||||
services.NewListMetricsService = func(provider models.MetricsClientProvider) models.ListMetricsProvider {
|
||||
return &mockListMetricsService
|
||||
}
|
||||
t.Run("calls GetMetricsByNamespace when a CustomNamespaceRequestType is passed", func(t *testing.T) {
|
||||
mockListMetricsService := mocks.ListMetricsServiceMock{}
|
||||
mockListMetricsService = mocks.ListMetricsServiceMock{}
|
||||
mockListMetricsService.On("GetMetricsByNamespace", mock.Anything).Return([]resources.ResourceResponse[resources.Metric]{}, nil)
|
||||
newListMetricsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
|
||||
return &mockListMetricsService, nil
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/metrics?region=us-east-2&namespace=customNamespace", nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(MetricsHandler, logger, nil))
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.MetricsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
mockListMetricsService.AssertNumberOfCalls(t, "GetMetricsByNamespace", 1)
|
||||
})
|
||||
@@ -42,7 +46,8 @@ func Test_Metrics_Route(t *testing.T) {
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/metrics?region=us-east-2", nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(MetricsHandler, logger, nil))
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.MetricsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.True(t, haveBeenCalled)
|
||||
})
|
||||
@@ -61,21 +66,20 @@ func Test_Metrics_Route(t *testing.T) {
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/metrics?region=us-east-2&namespace=AWS/DMS", nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(MetricsHandler, logger, nil))
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.MetricsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.True(t, haveBeenCalled)
|
||||
assert.Equal(t, "AWS/DMS", usedNamespace)
|
||||
})
|
||||
|
||||
t.Run("returns 500 if GetMetricsByNamespace returns an error", func(t *testing.T) {
|
||||
mockListMetricsService := mocks.ListMetricsServiceMock{}
|
||||
mockListMetricsService = mocks.ListMetricsServiceMock{}
|
||||
mockListMetricsService.On("GetMetricsByNamespace", mock.Anything).Return([]resources.ResourceResponse[resources.Metric]{}, fmt.Errorf("some error"))
|
||||
newListMetricsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
|
||||
return &mockListMetricsService, nil
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/metrics?region=us-east-2&namespace=customNamespace", nil)
|
||||
handler := http.HandlerFunc(ResourceRequestMiddleware(MetricsHandler, logger, nil))
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(ds.MetricsHandler))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
assert.Equal(t, `{"Message":"error in MetricsHandler: some error","Error":"some error","StatusCode":500}`, rr.Body.String())
|
||||
39
pkg/tsdb/cloudwatch/middleware_test.go
Normal file
39
pkg/tsdb/cloudwatch/middleware_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
)
|
||||
|
||||
func Test_Middleware(t *testing.T) {
|
||||
t.Run("rejects POST method", func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/dimension-keys?region=us-east-1", nil)
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(func(_ context.Context, parameters url.Values) ([]byte, *models.HttpError) {
|
||||
return []byte{}, nil
|
||||
}))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
|
||||
})
|
||||
|
||||
t.Run("should propagate handler error to response", func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/some-path", nil)
|
||||
ds := newTestDatasource()
|
||||
handler := http.HandlerFunc(ds.resourceRequestMiddleware(func(_ context.Context, parameters url.Values) ([]byte, *models.HttpError) {
|
||||
return []byte{}, models.NewHttpError("error", http.StatusBadRequest, fmt.Errorf("error from handler"))
|
||||
}))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Equal(t, `{"Message":"error: error from handler","Error":"error from handler","StatusCode":400}`, rr.Body.String())
|
||||
})
|
||||
}
|
||||
@@ -11,7 +11,7 @@ type AccountsServiceMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (a *AccountsServiceMock) GetAccountsForCurrentUserOrRole(ctx context.Context) ([]resources.ResourceResponse[resources.Account], error) {
|
||||
func (a *AccountsServiceMock) GetAccountsForCurrentUserOrRole(_ context.Context) ([]resources.ResourceResponse[resources.Account], error) {
|
||||
args := a.Called()
|
||||
|
||||
return args.Get(0).([]resources.ResourceResponse[resources.Account]), args.Error(1)
|
||||
|
||||
@@ -1,68 +1,65 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
|
||||
cloudwatchtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type FakeMetricsAPI struct {
|
||||
Metrics []*cloudwatch.Metric
|
||||
OwningAccounts []*string
|
||||
models.CWClient
|
||||
|
||||
Metrics []cloudwatchtypes.Metric
|
||||
OwningAccounts []string
|
||||
MetricsPerPage int
|
||||
|
||||
cursor int
|
||||
}
|
||||
|
||||
func (c *FakeMetricsAPI) ListMetricsPagesWithContext(ctx aws.Context, input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool, opts ...request.Option) error {
|
||||
func (c *FakeMetricsAPI) ListMetrics(_ context.Context, _ *cloudwatch.ListMetricsInput, _ ...func(*cloudwatch.Options)) (*cloudwatch.ListMetricsOutput, error) {
|
||||
if c.MetricsPerPage == 0 {
|
||||
c.MetricsPerPage = 1000
|
||||
}
|
||||
chunks := chunkSlice(c.Metrics, c.MetricsPerPage)
|
||||
|
||||
for i, metrics := range chunks {
|
||||
response := fn(&cloudwatch.ListMetricsOutput{
|
||||
Metrics: metrics,
|
||||
OwningAccounts: c.OwningAccounts,
|
||||
}, i+1 == len(chunks))
|
||||
if !response {
|
||||
break
|
||||
var metrics []cloudwatchtypes.Metric
|
||||
nextToken := aws.String("yes")
|
||||
if c.cursor < len(c.Metrics) {
|
||||
end := c.cursor + c.MetricsPerPage
|
||||
if end > len(c.Metrics) {
|
||||
end = len(c.Metrics)
|
||||
nextToken = nil
|
||||
}
|
||||
metrics = c.Metrics[c.cursor:end]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
c.cursor += c.MetricsPerPage
|
||||
|
||||
func chunkSlice(slice []*cloudwatch.Metric, chunkSize int) [][]*cloudwatch.Metric {
|
||||
var chunks [][]*cloudwatch.Metric
|
||||
for len(slice) != 0 {
|
||||
if len(slice) < chunkSize {
|
||||
chunkSize = len(slice)
|
||||
}
|
||||
|
||||
chunks = append(chunks, slice[0:chunkSize])
|
||||
slice = slice[chunkSize:]
|
||||
}
|
||||
|
||||
return chunks
|
||||
return &cloudwatch.ListMetricsOutput{
|
||||
Metrics: metrics,
|
||||
OwningAccounts: c.OwningAccounts,
|
||||
NextToken: nextToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MetricsAPI struct {
|
||||
cloudwatchiface.CloudWatchAPI
|
||||
mock.Mock
|
||||
models.CWClient
|
||||
|
||||
Metrics []*cloudwatch.Metric
|
||||
Metrics []cloudwatchtypes.Metric
|
||||
}
|
||||
|
||||
func (m *MetricsAPI) GetMetricDataWithContext(ctx aws.Context, input *cloudwatch.GetMetricDataInput, opts ...request.Option) (*cloudwatch.GetMetricDataOutput, error) {
|
||||
args := m.Called(ctx, input, opts)
|
||||
func (m *MetricsAPI) GetMetricData(ctx context.Context, input *cloudwatch.GetMetricDataInput, optFns ...func(*cloudwatch.Options)) (*cloudwatch.GetMetricDataOutput, error) {
|
||||
args := m.Called(ctx, input, optFns)
|
||||
|
||||
return args.Get(0).(*cloudwatch.GetMetricDataOutput), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MetricsAPI) ListMetricsPagesWithContext(ctx aws.Context, input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool, opts ...request.Option) error {
|
||||
fn(&cloudwatch.ListMetricsOutput{
|
||||
func (m *MetricsAPI) ListMetrics(_ context.Context, _ *cloudwatch.ListMetricsInput, _ ...func(*cloudwatch.Options)) (*cloudwatch.ListMetricsOutput, error) {
|
||||
return &cloudwatch.ListMetricsOutput{
|
||||
Metrics: m.Metrics,
|
||||
}, true)
|
||||
|
||||
return m.Called().Error(0)
|
||||
}, m.Called().Error(0)
|
||||
}
|
||||
|
||||
@@ -11,19 +11,19 @@ type ListMetricsServiceMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (a *ListMetricsServiceMock) GetDimensionKeysByDimensionFilter(ctx context.Context, r resources.DimensionKeysRequest) ([]resources.ResourceResponse[string], error) {
|
||||
func (a *ListMetricsServiceMock) GetDimensionKeysByDimensionFilter(_ context.Context, r resources.DimensionKeysRequest) ([]resources.ResourceResponse[string], error) {
|
||||
args := a.Called(r)
|
||||
|
||||
return args.Get(0).([]resources.ResourceResponse[string]), args.Error(1)
|
||||
}
|
||||
|
||||
func (a *ListMetricsServiceMock) GetDimensionValuesByDimensionFilter(ctx context.Context, r resources.DimensionValuesRequest) ([]resources.ResourceResponse[string], error) {
|
||||
func (a *ListMetricsServiceMock) GetDimensionValuesByDimensionFilter(_ context.Context, r resources.DimensionValuesRequest) ([]resources.ResourceResponse[string], error) {
|
||||
args := a.Called(r)
|
||||
|
||||
return args.Get(0).([]resources.ResourceResponse[string]), args.Error(1)
|
||||
}
|
||||
|
||||
func (a *ListMetricsServiceMock) GetMetricsByNamespace(ctx context.Context, r resources.MetricsRequest) ([]resources.ResourceResponse[resources.Metric], error) {
|
||||
func (a *ListMetricsServiceMock) GetMetricsByNamespace(_ context.Context, r resources.MetricsRequest) ([]resources.ResourceResponse[resources.Metric], error) {
|
||||
args := a.Called(r)
|
||||
|
||||
return args.Get(0).([]resources.ResourceResponse[resources.Metric]), args.Error(1)
|
||||
|
||||
@@ -3,10 +3,7 @@ package mocks
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
|
||||
@@ -16,13 +13,13 @@ type LogsAPI struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (l *LogsAPI) DescribeLogGroupsWithContext(ctx context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, option ...request.Option) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
|
||||
func (l *LogsAPI) DescribeLogGroups(_ context.Context, input *cloudwatchlogs.DescribeLogGroupsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
|
||||
args := l.Called(input)
|
||||
|
||||
return args.Get(0).(*cloudwatchlogs.DescribeLogGroupsOutput), args.Error(1)
|
||||
}
|
||||
|
||||
func (l *LogsAPI) GetLogGroupFieldsWithContext(ctx context.Context, input *cloudwatchlogs.GetLogGroupFieldsInput, option ...request.Option) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) {
|
||||
func (l *LogsAPI) GetLogGroupFields(_ context.Context, input *cloudwatchlogs.GetLogGroupFieldsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetLogGroupFieldsOutput, error) {
|
||||
args := l.Called(input)
|
||||
|
||||
return args.Get(0).(*cloudwatchlogs.GetLogGroupFieldsOutput), args.Error(1)
|
||||
@@ -32,26 +29,40 @@ type LogsService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (l *LogsService) GetLogGroupsWithContext(ctx context.Context, request resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error) {
|
||||
func (l *LogsService) GetLogGroups(_ context.Context, request resources.LogGroupsRequest) ([]resources.ResourceResponse[resources.LogGroup], error) {
|
||||
args := l.Called(request)
|
||||
|
||||
return args.Get(0).([]resources.ResourceResponse[resources.LogGroup]), args.Error(1)
|
||||
}
|
||||
|
||||
func (l *LogsService) GetLogGroupFieldsWithContext(ctx context.Context, request resources.LogGroupFieldsRequest, option ...request.Option) ([]resources.ResourceResponse[resources.LogGroupField], error) {
|
||||
func (l *LogsService) GetLogGroupFields(_ context.Context, request resources.LogGroupFieldsRequest) ([]resources.ResourceResponse[resources.LogGroupField], error) {
|
||||
args := l.Called(request)
|
||||
|
||||
return args.Get(0).([]resources.ResourceResponse[resources.LogGroupField]), args.Error(1)
|
||||
}
|
||||
|
||||
type MockLogEvents struct {
|
||||
cloudwatchlogsiface.CloudWatchLogsAPI
|
||||
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockLogEvents) GetLogEventsWithContext(ctx aws.Context, input *cloudwatchlogs.GetLogEventsInput, option ...request.Option) (*cloudwatchlogs.GetLogEventsOutput, error) {
|
||||
args := m.Called(ctx, input, option)
|
||||
func (m *MockLogEvents) StartQuery(context.Context, *cloudwatchlogs.StartQueryInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.StartQueryOutput, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockLogEvents) StopQuery(context.Context, *cloudwatchlogs.StopQueryInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.StopQueryOutput, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockLogEvents) GetQueryResults(context.Context, *cloudwatchlogs.GetQueryResultsInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetQueryResultsOutput, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockLogEvents) DescribeLogGroups(context.Context, *cloudwatchlogs.DescribeLogGroupsInput, ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockLogEvents) GetLogEvents(ctx context.Context, input *cloudwatchlogs.GetLogEventsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetLogEventsOutput, error) {
|
||||
args := m.Called(ctx, input, optFns)
|
||||
|
||||
return args.Get(0).(*cloudwatchlogs.GetLogEventsOutput), args.Error(1)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user