Compare commits
45 Commits
jacobsonmt
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80ef886fb4 | ||
|
|
93f1c24b82 | ||
|
|
7c2f38641a | ||
|
|
6376484b13 | ||
|
|
d1334a6dff | ||
|
|
505e025d18 | ||
|
|
571e5c2e3c | ||
|
|
4ceb7eec52 | ||
|
|
2e507d5042 | ||
|
|
1bd5b29963 | ||
|
|
98ec655f33 | ||
|
|
1d38cf7f0d | ||
|
|
891ed6c4b7 | ||
|
|
e067b1de98 | ||
|
|
4cecab3185 | ||
|
|
867e8bb98f | ||
|
|
5abc0d0d91 | ||
|
|
3972046695 | ||
|
|
ffa5e41bec | ||
|
|
84bd99f1c1 | ||
|
|
de512fb02e | ||
|
|
3fca7cf952 | ||
|
|
8f8ed2bbec | ||
|
|
69e4b4667b | ||
|
|
0c016e210a | ||
|
|
769787ea40 | ||
|
|
b4312a220f | ||
|
|
c75a451b13 | ||
|
|
df816d00e4 | ||
|
|
782c819727 | ||
|
|
6fc82629d4 | ||
|
|
fc34b18122 | ||
|
|
ac162e203b | ||
|
|
ddeb970b4d | ||
|
|
a06905e2d3 | ||
|
|
07bf7b2ae1 | ||
|
|
0649635639 | ||
|
|
126e6dbcd8 | ||
|
|
f56fec2c10 | ||
|
|
ca5cf5fe7c | ||
|
|
06fb3fef43 | ||
|
|
414524e799 | ||
|
|
acf0da9b80 | ||
|
|
6f7c525213 | ||
|
|
684156fdf1 |
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -1 +1,6 @@
|
||||
* text=auto eol=lf
|
||||
*.gen.ts linguist-generated
|
||||
*_gen.ts linguist-generated
|
||||
*_gen.go linguist-generated
|
||||
**/openapi_snapshots/*.json linguist-generated
|
||||
apps/**/pkg/apis/*_manifest.go linguist-generated
|
||||
|
||||
21
.github/CODEOWNERS
vendored
21
.github/CODEOWNERS
vendored
@@ -151,7 +151,7 @@
|
||||
/pkg/promlib @grafana/oss-big-tent
|
||||
/pkg/storage/ @grafana/grafana-search-and-storage
|
||||
/pkg/storage/secret/ @grafana/grafana-operator-experience-squad
|
||||
/pkg/services/annotations/ @grafana/grafana-search-and-storage
|
||||
/pkg/services/annotations/ @grafana/grafana-backend-services-squad
|
||||
/pkg/services/apikey/ @grafana/identity-squad
|
||||
/pkg/services/cleanup/ @grafana/grafana-backend-group
|
||||
/pkg/services/contexthandler/ @grafana/grafana-backend-group @grafana/grafana-app-platform-squad
|
||||
@@ -181,7 +181,7 @@
|
||||
/pkg/services/search/ @grafana/grafana-search-and-storage
|
||||
/pkg/services/searchusers/ @grafana/grafana-search-and-storage
|
||||
/pkg/services/secrets/ @grafana/grafana-operator-experience-squad
|
||||
/pkg/services/shorturls/ @grafana/grafana-backend-group
|
||||
/pkg/services/shorturls/ @grafana/sharing-squad
|
||||
/pkg/services/sqlstore/ @grafana/grafana-search-and-storage
|
||||
/pkg/services/ssosettings/ @grafana/identity-squad
|
||||
/pkg/services/star/ @grafana/grafana-search-and-storage
|
||||
@@ -199,6 +199,7 @@
|
||||
/pkg/tests/apis/features @grafana/grafana-backend-services-squad
|
||||
/pkg/tests/apis/folder @grafana/grafana-search-and-storage
|
||||
/pkg/tests/apis/iam @grafana/identity-access-team
|
||||
/pkg/tests/apis/shorturl @grafana/sharing-squad
|
||||
/pkg/tests/api/correlations/ @grafana/datapro
|
||||
/pkg/tsdb/grafanads/ @grafana/grafana-backend-group
|
||||
/pkg/tsdb/opentsdb/ @grafana/partner-datasources
|
||||
@@ -241,6 +242,7 @@
|
||||
/devenv/dev-dashboards/panel-library @grafana/dataviz-squad
|
||||
/devenv/dev-dashboards/panel-piechart @grafana/dataviz-squad
|
||||
/devenv/dev-dashboards/panel-stat @grafana/dataviz-squad
|
||||
/devenv/dev-dashboards/panel-status-history @grafana/dataviz-squad
|
||||
/devenv/dev-dashboards/panel-table @grafana/dataviz-squad
|
||||
/devenv/dev-dashboards/panel-timeline @grafana/dataviz-squad
|
||||
/devenv/dev-dashboards/panel-timeseries @grafana/dataviz-squad
|
||||
@@ -472,24 +474,12 @@ i18next.config.ts @grafana/grafana-frontend-platform
|
||||
/e2e-playwright/fixtures/long-trace-response.json @grafana/observability-traces-and-profiling
|
||||
/e2e-playwright/fixtures/tempo-response.json @grafana/oss-big-tent
|
||||
/e2e-playwright/fixtures/prometheus-response.json @grafana/datapro
|
||||
/e2e-playwright/panels-suite/canvas-scene.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/ @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/dashlist.spec.ts @grafana/grafana-search-navigate-organise
|
||||
/e2e-playwright/panels-suite/datagrid-data-change.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/datagrid-editing-features.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts @grafana/plugins-platform-frontend
|
||||
/e2e-playwright/panels-suite/geomap-layer-types.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/geomap-map-controls.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/heatmap.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/panelEdit_base.spec.ts @grafana/dashboards-squad
|
||||
/e2e-playwright/panels-suite/panelEdit_queries.spec.ts @grafana/dashboards-squad
|
||||
/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts @grafana/datapro
|
||||
/e2e-playwright/panels-suite/state-timeline.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/table-footer.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/table-kitchenSink.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/table-markdown.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/table-sparkline.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/table-utils.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/plugin-e2e/ @grafana/oss-big-tent @grafana/partner-datasources
|
||||
/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend
|
||||
/e2e-playwright/smoke-tests-suite/ @grafana/grafana-frontend-platform
|
||||
@@ -1176,6 +1166,7 @@ embed.go @grafana/grafana-as-code
|
||||
/pkg/registry/ @grafana/grafana-as-code
|
||||
/pkg/registry/apis/ @grafana/grafana-app-platform-squad
|
||||
/pkg/registry/apis/folders @grafana/grafana-search-and-storage
|
||||
/pkg/registry/apis/datasource @grafana/grafana-datasources-core-services
|
||||
/pkg/registry/apis/query @grafana/grafana-datasources-core-services
|
||||
/pkg/registry/apis/secret @grafana/grafana-operator-experience-squad
|
||||
/pkg/registry/apis/userstorage @grafana/grafana-app-platform-squad @grafana/plugins-platform-backend
|
||||
|
||||
8
.github/actions/change-detection/action.yml
vendored
8
.github/actions/change-detection/action.yml
vendored
@@ -31,6 +31,9 @@ outputs:
|
||||
dockerfile:
|
||||
description: Whether the dockerfile or self have changed in any way
|
||||
value: ${{ steps.changed-files.outputs.dockerfile_any_changed || 'true' }}
|
||||
devenv:
|
||||
description: Whether the devenv or self have changed in any way
|
||||
value: ${{ steps.changed-files.outputs.devenv_any_changed || 'true' }}
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
@@ -136,6 +139,9 @@ runs:
|
||||
- '.vale.ini'
|
||||
- '.github/actions/change-detection/**'
|
||||
- '${{ inputs.self }}'
|
||||
devenv:
|
||||
- 'devenv/**'
|
||||
- '${{ inputs.self }}'
|
||||
- name: Print all change groups
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -157,3 +163,5 @@ runs:
|
||||
echo " --> ${{ steps.changed-files.outputs.docs_all_changed_files }}"
|
||||
echo "Dockerfile: ${{ steps.changed-files.outputs.dockerfile_any_changed || 'true' }}"
|
||||
echo " --> ${{ steps.changed-files.outputs.dockerfile_all_changed_files }}"
|
||||
echo "devenv: ${{ steps.changed-files.outputs.devenv_any_changed || 'true' }}"
|
||||
echo " --> ${{ steps.changed-files.outputs.devenv_all_changed_files }}"
|
||||
|
||||
4
.github/renovate.json5
vendored
4
.github/renovate.json5
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
extends: ["config:recommended"],
|
||||
enabledManagers: ["npm"],
|
||||
enabledManagers: ["npm", "docker-compose"],
|
||||
ignorePresets: [
|
||||
"github>grafana/grafana-renovate-config//presets/labels",
|
||||
],
|
||||
@@ -26,7 +26,7 @@
|
||||
"@types/slate-react", // we don't want to continue using this on the long run, use Monaco editor instead of Slate
|
||||
"@types/slate", // we don't want to continue using this on the long run, use Monaco editor instead of Slate
|
||||
],
|
||||
includePaths: ["package.json", "packages/**", "public/app/plugins/**"],
|
||||
includePaths: ["package.json", "packages/**", "public/app/plugins/**", "devenv/frontend-service/docker-compose.yaml"],
|
||||
ignorePaths: ["emails/**", "**/mocks/**"],
|
||||
labels: ["area/frontend", "dependencies", "no-changelog"],
|
||||
postUpdateOptions: ["yarnDedupeHighest"],
|
||||
|
||||
2
.github/workflows/backport-trigger.yml
vendored
2
.github/workflows/backport-trigger.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
}' "$GITHUB_EVENT_PATH" > /tmp/pr_info.json
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: pr_info
|
||||
path: /tmp/pr_info.json
|
||||
|
||||
@@ -193,7 +193,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
- name: store build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: ${{ steps.get_dir.outputs.dir }}/ci/packages/*.zip
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
run: zip -r ./pr_built_packages.zip ./packages/**/*.tgz
|
||||
|
||||
- name: Upload build output as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: buildPr
|
||||
path: './pr/pr_built_packages.zip'
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
run: zip -r ./base_built_packages.zip ./packages/**/*.tgz
|
||||
|
||||
- name: Upload build output as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: buildBase
|
||||
path: './base/base_built_packages.zip'
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
- name: Upload check output as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: levitate
|
||||
path: levitate/
|
||||
|
||||
14
.github/workflows/pr-e2e-tests.yml
vendored
14
.github/workflows/pr-e2e-tests.yml
vendored
@@ -94,14 +94,14 @@ jobs:
|
||||
id: artifact
|
||||
|
||||
- name: Upload grafana.tar.gz
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
retention-days: 1
|
||||
name: grafana-tar-gz
|
||||
path: build-dir/grafana.tar.gz
|
||||
|
||||
- name: Upload grafana docker tarball
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
retention-days: 1
|
||||
name: grafana-docker-tar-gz
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
# We want a static binary, so we need to set CGO_ENABLED=0
|
||||
CGO_ENABLED=0 go build -o ./e2e-runner ./e2e/
|
||||
echo "artifact=e2e-runner-${{github.run_number}}" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v5
|
||||
id: upload
|
||||
with:
|
||||
retention-days: 1
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "suite=$(echo "$SUITE" | sed 's/\//-/g')" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v5
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: ${{ steps.set-suite-name.outputs.suite }}-${{ github.run_number }}
|
||||
@@ -307,7 +307,7 @@ jobs:
|
||||
version: 0.18.8
|
||||
verb: run
|
||||
args: go run ./pkg/build/e2e-playwright --package=grafana.tar.gz --shard=${{ matrix.shard }}/${{ matrix.shardTotal }} --blob-dir=./blob-report
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v5
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: playwright-blob-${{ github.run_number }}-${{ matrix.shard }}
|
||||
@@ -439,7 +439,7 @@ jobs:
|
||||
|
||||
- name: Upload HTML report
|
||||
id: upload-html
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: playwright-html-${{ github.run_number }}
|
||||
path: playwright-report
|
||||
@@ -498,7 +498,7 @@ jobs:
|
||||
args: go run ./pkg/build/a11y --package=grafana.tar.gz --no-threshold-fail --results=./pa11y-ci-results.json
|
||||
- name: Upload pa11y results
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
retention-days: 1
|
||||
name: pa11y-ci-results
|
||||
|
||||
24
.github/workflows/pr-frontend-unit-tests.yml
vendored
24
.github/workflows/pr-frontend-unit-tests.yml
vendored
@@ -18,6 +18,7 @@ jobs:
|
||||
contents: read
|
||||
outputs:
|
||||
changed: ${{ steps.detect-changes.outputs.frontend }}
|
||||
devenv-changed: ${{ steps.detect-changes.outputs.devenv }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -169,3 +170,26 @@ jobs:
|
||||
needs: ${{ toJson(needs) }}
|
||||
failure-message: "One or more unit test jobs have failed"
|
||||
success-message: "All unit tests completed successfully"
|
||||
|
||||
devenv:
|
||||
needs:
|
||||
- detect-changes
|
||||
if: needs.detect-changes.outputs.devenv-changed == 'true'
|
||||
runs-on: ubuntu-x64-large
|
||||
name: "Devenv frontend-service build"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Docker
|
||||
uses: docker/setup-docker-action@efe9e3891a4f7307e689f2100b33a155b900a608 # v4
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Install Tilt
|
||||
run: curl -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash
|
||||
- name: Create empty config files # TODO: the tiltfile should conditionally mount these only if they exist, like the enterprise license
|
||||
run: |
|
||||
touch devenv/frontend-service/configs/grafana-api.local.ini
|
||||
touch devenv/frontend-service/configs/frontend-service.local.ini
|
||||
- name: Test frontend-service Tiltfile
|
||||
run: tilt ci --file devenv/frontend-service/Tiltfile
|
||||
|
||||
2
.github/workflows/pr-test-docker.yml
vendored
2
.github/workflows/pr-test-docker.yml
vendored
@@ -34,6 +34,6 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: docker/setup-docker-action@3fb92d6d9c634363128c8cce4bc3b2826526370a # v4
|
||||
- uses: docker/setup-docker-action@efe9e3891a4f7307e689f2100b33a155b900a608 # v4
|
||||
- name: Build Dockerfile
|
||||
run: make build-docker-full
|
||||
|
||||
62
.github/workflows/pr-test-integration.yml
vendored
62
.github/workflows/pr-test-integration.yml
vendored
@@ -78,6 +78,7 @@ jobs:
|
||||
# We don't need more than this since it has to wait for the other tests.
|
||||
shard: [
|
||||
1/4, 2/4, 3/4, 4/4,
|
||||
profiled,
|
||||
]
|
||||
fail-fast: false
|
||||
|
||||
@@ -96,13 +97,68 @@ jobs:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- name: Run tests
|
||||
if: matrix.shard != 'profiled'
|
||||
env:
|
||||
SHARD: ${{ matrix.shard }}
|
||||
CGO_ENABLED: 0
|
||||
SKIP_PACKAGES: |-
|
||||
pkg/tests/apis/folder
|
||||
pkg/tests/apis/dashboard
|
||||
run: |
|
||||
set -euo pipefail
|
||||
readarray -t PACKAGES <<< "$(./scripts/ci/backend-tests/pkgs-with-tests-named.sh -b TestIntegration | ./scripts/ci/backend-tests/shard.sh -N"$SHARD" -d-)"
|
||||
# ionice since tests are IO intensive
|
||||
CGO_ENABLED=0 ionice -c2 -n7 go test -p=4 -tags=sqlite -timeout=8m -run '^TestIntegration' "${PACKAGES[@]}"
|
||||
# Build regex pattern like: pkg1$|pkg2$|pkg3$
|
||||
SKIP_PATTERN=$(echo "$SKIP_PACKAGES" | sed '/^$/d' | sed 's|.*|&$|' | paste -sd '|' -)
|
||||
readarray -t PACKAGES <<< "$(./scripts/ci/backend-tests/pkgs-with-tests-named.sh -b TestIntegration | ./scripts/ci/backend-tests/shard.sh -N "$SHARD" -d - | grep -Ev "($SKIP_PATTERN)")"
|
||||
go test -tags=sqlite -timeout=8m -run '^TestIntegration' "${PACKAGES[@]}"
|
||||
- name: Run profiled tests
|
||||
id: run-profiled-tests
|
||||
if: matrix.shard == 'profiled'
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
PROFILED_PACKAGES: |-
|
||||
pkg/tests/apis/folder
|
||||
pkg/tests/apis/dashboard
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Build regex pattern line: pkg1$|pkg2$|pkg3$
|
||||
PROFILE_PATTERN=$(echo "$PROFILED_PACKAGES" | sed '/^$/d' | sed 's|.*|&$|' | paste -sd '|' -)
|
||||
readarray -t PACKAGES <<< "$(./scripts/ci/backend-tests/pkgs-with-tests-named.sh -b TestIntegration | grep -E "($PROFILE_PATTERN)")"
|
||||
if [ ${#PACKAGES[@]} -eq 0 ]; then
|
||||
echo "⚠️ No profiled packages found"
|
||||
exit 0
|
||||
fi
|
||||
mkdir -p profiles
|
||||
EXIT_CODE=0
|
||||
# Run each profiled package sequentially
|
||||
for full_pkg in "${PACKAGES[@]}"; do
|
||||
# Build valid file name
|
||||
pkg_name=$(basename "$full_pkg" | tr '/' '_' | tr '.' '_')
|
||||
echo "📦 Running $full_pkg"
|
||||
set +e
|
||||
go test -tags=sqlite -timeout=8m -run '^TestIntegration' \
|
||||
-outputdir=profiles \
|
||||
-cpuprofile="cpu_${pkg_name}.prof" \
|
||||
-memprofile="mem_${pkg_name}.prof" \
|
||||
-trace="trace_${pkg_name}.out" \
|
||||
"$full_pkg" 2>&1 | tee "profiles/test_${pkg_name}.log"
|
||||
TEST_EXIT=$?
|
||||
set -e
|
||||
if [ $TEST_EXIT -ne 0 ]; then
|
||||
echo "❌ $full_pkg failed with exit code $TEST_EXIT"
|
||||
EXIT_CODE=1
|
||||
else
|
||||
echo "✅ $full_pkg passed"
|
||||
fi
|
||||
done
|
||||
exit $EXIT_CODE
|
||||
- name: Output test profiles and traces
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v4
|
||||
if: (matrix.shard == 'profiled' && !cancelled())
|
||||
with:
|
||||
name: integration-test-profiles-sqlite-nocgo-${{ github.run_number }}
|
||||
path: profiles/
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
mysql:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.changed == 'true'
|
||||
|
||||
4
.github/workflows/release-build.yml
vendored
4
.github/workflows/release-build.yml
vendored
@@ -187,12 +187,12 @@ jobs:
|
||||
output: artifacts-${{ matrix.name }}.txt
|
||||
verify: ${{ matrix.verify }}
|
||||
build-id: ${{ github.run_id }}
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
with:
|
||||
name: artifacts-list-${{ matrix.name }}
|
||||
path: ${{ steps.build.outputs.file }}
|
||||
retention-days: 1
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
with:
|
||||
name: artifacts-${{ matrix.name }}
|
||||
path: ${{ steps.build.outputs.dist-dir }}
|
||||
|
||||
4
.github/workflows/storybook-a11y.yml
vendored
4
.github/workflows/storybook-a11y.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
id-token: write
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.changed == 'true'
|
||||
name: "Run Storybook a11y tests"
|
||||
name: "Run Storybook a11y tests (light theme)"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
id-token: write
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.changed == 'true'
|
||||
name: "Run Storybook a11y tests"
|
||||
name: "Run Storybook a11y tests (dark theme)"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
|
||||
@@ -15,3 +15,20 @@ generate: install-app-sdk update-app-sdk
|
||||
.PHONY: run
|
||||
run:
|
||||
@go run ./pkg/standalone/server.go --etcd-servers=http://127.0.0.1:22379 --secure-port 7445
|
||||
|
||||
.PHONY: create-checks
|
||||
create-checks:
|
||||
@echo "Creating plugin check..."
|
||||
@curl -k -X POST https://localhost:7445/apis/advisor.grafana.app/v0alpha1/namespaces/stacks-1/checks \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"kind":"Check","apiVersion":"advisor.grafana.app/v0alpha1","spec":{"data":{}},"metadata":{"generateName":"check-","labels":{"advisor.grafana.app/type":"plugin"},"namespace":"stacks-1"},"status":{"report":{"count":0,"failures":[]}}}' \
|
||||
&& echo "Plugin check created successfully"
|
||||
@echo "Creating datasource check..."
|
||||
@curl -k -X POST https://localhost:7445/apis/advisor.grafana.app/v0alpha1/namespaces/stacks-1/checks \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"kind":"Check","apiVersion":"advisor.grafana.app/v0alpha1","spec":{"data":{}},"metadata":{"generateName":"check-","labels":{"advisor.grafana.app/type":"datasource"},"namespace":"stacks-1"},"status":{"report":{"count":0,"failures":[]}}}' \
|
||||
&& echo "Datasource check created successfully"
|
||||
|
||||
delete-checks:
|
||||
@curl -k -X DELETE https://localhost:7445/apis/advisor.grafana.app/v0alpha1/namespaces/stacks-1/checks \
|
||||
&& echo "All checks deleted successfully"
|
||||
|
||||
@@ -163,3 +163,17 @@ make run # Start the advisor app in standalone mode
|
||||
```
|
||||
|
||||
This will start the advisor app on port 7445. You can then access the advisor app at `http://localhost:7445`.
|
||||
|
||||
To see some sample checks, you can run the following command:
|
||||
|
||||
```bash
|
||||
make create-checks
|
||||
```
|
||||
|
||||
Then you can see list in the URL: `http://localhost:7445/apis/advisor.grafana.app/v0alpha1/namespaces/stacks-1/checks`
|
||||
|
||||
To delete all checks, you can run the following command:
|
||||
|
||||
```bash
|
||||
make delete-checks
|
||||
```
|
||||
|
||||
@@ -1,12 +1,59 @@
|
||||
package mockchecks
|
||||
|
||||
import "github.com/grafana/grafana/apps/advisor/pkg/app/checks"
|
||||
import (
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry/mockchecks/mocksvcs"
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checks/datasourcecheck"
|
||||
"github.com/grafana/grafana/apps/advisor/pkg/app/checks/plugincheck"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
)
|
||||
|
||||
// mockchecks.CheckRegistry is a mock implementation of the checkregistry.CheckService interface
|
||||
// TODO: Add mocked checks here
|
||||
type CheckRegistry struct {
|
||||
datasourceSvc datasources.DataSourceService
|
||||
pluginStore pluginstore.Store
|
||||
pluginClient plugins.Client
|
||||
pluginRepo repo.Service
|
||||
GrafanaVersion string
|
||||
pluginContextProvider datasourcecheck.PluginContextProvider
|
||||
updateChecker pluginchecker.PluginUpdateChecker
|
||||
pluginErrorResolver plugins.ErrorResolver
|
||||
}
|
||||
|
||||
func (m *CheckRegistry) Checks() []checks.Check {
|
||||
return []checks.Check{}
|
||||
return []checks.Check{
|
||||
datasourcecheck.New(
|
||||
m.datasourceSvc,
|
||||
m.pluginStore,
|
||||
m.pluginContextProvider,
|
||||
m.pluginClient,
|
||||
m.pluginRepo,
|
||||
m.GrafanaVersion,
|
||||
),
|
||||
plugincheck.New(
|
||||
m.pluginStore,
|
||||
m.pluginRepo,
|
||||
m.updateChecker,
|
||||
m.pluginErrorResolver,
|
||||
m.GrafanaVersion,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func New() *CheckRegistry {
|
||||
return &CheckRegistry{
|
||||
datasourceSvc: &mocksvcs.DatasourceSvc{},
|
||||
pluginStore: &mocksvcs.PluginStore{},
|
||||
pluginClient: &mocksvcs.PluginClient{},
|
||||
pluginRepo: &mocksvcs.PluginRepo{},
|
||||
pluginContextProvider: &mocksvcs.PluginContextProvider{},
|
||||
updateChecker: &mocksvcs.UpdateChecker{},
|
||||
pluginErrorResolver: &mocksvcs.PluginErrorResolver{},
|
||||
GrafanaVersion: "1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package mocksvcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
)
|
||||
|
||||
var dss = map[string]*datasources.DataSource{
|
||||
"prometheus-uid": {
|
||||
ID: 1,
|
||||
UID: "prometheus-uid",
|
||||
Name: "Prometheus",
|
||||
Type: "prometheus",
|
||||
},
|
||||
"mysql-uid": {
|
||||
ID: 2,
|
||||
UID: "mysql-uid",
|
||||
Name: "MySQL",
|
||||
Type: "mysql",
|
||||
},
|
||||
"unknown-uid": {
|
||||
ID: 3,
|
||||
UID: "unknown-uid",
|
||||
Name: "Unknown",
|
||||
Type: "unknown",
|
||||
},
|
||||
}
|
||||
|
||||
type DatasourceSvc struct {
|
||||
datasources.DataSourceService
|
||||
}
|
||||
|
||||
func (m *DatasourceSvc) GetDataSources(ctx context.Context, query *datasources.GetDataSourcesQuery) ([]*datasources.DataSource, error) {
|
||||
sources := make([]*datasources.DataSource, 0, len(dss))
|
||||
for _, ds := range dss {
|
||||
sources = append(sources, ds)
|
||||
}
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func (m *DatasourceSvc) GetDataSource(ctx context.Context, query *datasources.GetDataSourceQuery) (*datasources.DataSource, error) {
|
||||
return dss[query.UID], nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package mocksvcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
type PluginClient struct {
|
||||
plugins.Client
|
||||
}
|
||||
|
||||
func (m *PluginClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
return &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusOk,
|
||||
Message: "Plugin is healthy",
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package mocksvcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
)
|
||||
|
||||
type PluginContextProvider struct {
|
||||
}
|
||||
|
||||
// ACTUALLY USED by datasourcecheck
|
||||
func (m *PluginContextProvider) GetWithDataSource(ctx context.Context, pluginID string, user identity.Requester, ds *datasources.DataSource) (backend.PluginContext, error) {
|
||||
// Create a plugin context with sample data based on the datasource
|
||||
pluginContext := backend.PluginContext{
|
||||
PluginID: pluginID,
|
||||
PluginVersion: "1.0.0",
|
||||
OrgID: 1,
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
|
||||
ID: ds.ID,
|
||||
UID: ds.UID,
|
||||
Name: ds.Name,
|
||||
URL: ds.URL,
|
||||
JSONData: []byte(`{
|
||||
"httpMethod": "GET",
|
||||
"timeout": "30s",
|
||||
"keepCookies": []
|
||||
}`),
|
||||
DecryptedSecureJSONData: map[string]string{
|
||||
"password": "sample-password",
|
||||
"apiKey": "sample-api-key",
|
||||
},
|
||||
},
|
||||
GrafanaConfig: backend.NewGrafanaCfg(map[string]string{
|
||||
"app_url": "http://localhost:3000",
|
||||
"default_timezone": "UTC",
|
||||
}),
|
||||
}
|
||||
|
||||
// Add user context if provided
|
||||
if user != nil && !user.IsNil() {
|
||||
pluginContext.User = &backend.User{
|
||||
Login: user.GetLogin(),
|
||||
Name: user.GetName(),
|
||||
Email: user.GetEmail(),
|
||||
Role: string(user.GetOrgRole()),
|
||||
}
|
||||
}
|
||||
|
||||
return pluginContext, nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package mocksvcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
type PluginErrorResolver struct {
|
||||
}
|
||||
|
||||
// Assume no plugin with errors
|
||||
func (m *PluginErrorResolver) PluginErrors(ctx context.Context) []*plugins.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PluginErrorResolver) PluginError(ctx context.Context, pluginID string) *plugins.Error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package mocksvcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
)
|
||||
|
||||
type PluginRepo struct {
|
||||
repo.Service
|
||||
}
|
||||
|
||||
func (m *PluginRepo) GetPluginsInfo(ctx context.Context, options repo.GetPluginsInfoOptions, compatOpts repo.CompatOpts) ([]repo.PluginInfo, error) {
|
||||
return []repo.PluginInfo{
|
||||
{
|
||||
ID: 1,
|
||||
Slug: "grafana-piechart-panel",
|
||||
Version: "1.6.0",
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Slug: "prometheus",
|
||||
Version: "10.0.0",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package mocksvcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
)
|
||||
|
||||
type PluginStore struct {
|
||||
}
|
||||
|
||||
var ps = map[string]pluginstore.Plugin{
|
||||
"prometheus": {
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "prometheus",
|
||||
Type: plugins.TypeDataSource,
|
||||
Name: "Prometheus",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Grafana Labs",
|
||||
},
|
||||
Version: "10.0.0",
|
||||
},
|
||||
Category: "Time series databases",
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
Backend: true,
|
||||
Metrics: true,
|
||||
Logs: true,
|
||||
Alerting: true,
|
||||
Explore: true,
|
||||
},
|
||||
Class: plugins.ClassCore,
|
||||
Signature: plugins.SignatureStatusInternal,
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignatureOrg: "grafana.com",
|
||||
},
|
||||
"test-datasource": {
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "grafana-piechart-panel",
|
||||
Type: plugins.TypePanel,
|
||||
Name: "Pie Chart",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Grafana Labs",
|
||||
},
|
||||
Version: "1.6.0",
|
||||
},
|
||||
Category: "Visualization",
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
},
|
||||
Class: plugins.ClassCore,
|
||||
Signature: plugins.SignatureStatusInternal,
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignatureOrg: "grafana.com",
|
||||
},
|
||||
"grafana-piechart-panel": {
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "prometheus",
|
||||
Type: plugins.TypeDataSource,
|
||||
Name: "Prometheus",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Grafana Labs",
|
||||
},
|
||||
Version: "10.0.0",
|
||||
},
|
||||
Category: "Time series databases",
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
Backend: true,
|
||||
Metrics: true,
|
||||
Logs: true,
|
||||
Alerting: true,
|
||||
Explore: true,
|
||||
},
|
||||
Class: plugins.ClassCore,
|
||||
Signature: plugins.SignatureStatusInternal,
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignatureOrg: "grafana.com",
|
||||
},
|
||||
"test-app": {
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-app",
|
||||
Type: plugins.TypeApp,
|
||||
Name: "Test App",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Test Author",
|
||||
},
|
||||
Version: "2.0.0",
|
||||
},
|
||||
Category: "Application",
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
AutoEnabled: true,
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
SignatureType: plugins.SignatureTypeCommercial,
|
||||
SignatureOrg: "test.com",
|
||||
},
|
||||
}
|
||||
|
||||
func (s *PluginStore) Plugin(ctx context.Context, pluginID string) (pluginstore.Plugin, bool) {
|
||||
p, ok := ps[pluginID]
|
||||
return p, ok
|
||||
}
|
||||
|
||||
func (s *PluginStore) Plugins(ctx context.Context, pluginTypes ...plugins.Type) []pluginstore.Plugin {
|
||||
plugins := make([]pluginstore.Plugin, 0, len(ps))
|
||||
for _, p := range ps {
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
return plugins
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package mocksvcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
)
|
||||
|
||||
type UpdateChecker struct {
|
||||
}
|
||||
|
||||
func (m *UpdateChecker) IsUpdatable(ctx context.Context, plugin pluginstore.Plugin) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *UpdateChecker) CanUpdate(pluginId string, currentVersion string, targetVersion string, onlyMinor bool) bool {
|
||||
return true
|
||||
}
|
||||
@@ -26,7 +26,7 @@ const (
|
||||
type check struct {
|
||||
DatasourceSvc datasources.DataSourceService
|
||||
PluginStore pluginstore.Store
|
||||
PluginContextProvider pluginContextProvider
|
||||
PluginContextProvider PluginContextProvider
|
||||
PluginClient plugins.Client
|
||||
PluginRepo repo.Service
|
||||
GrafanaVersion string
|
||||
@@ -37,7 +37,7 @@ type check struct {
|
||||
func New(
|
||||
datasourceSvc datasources.DataSourceService,
|
||||
pluginStore pluginstore.Store,
|
||||
pluginContextProvider pluginContextProvider,
|
||||
pluginContextProvider PluginContextProvider,
|
||||
pluginClient plugins.Client,
|
||||
pluginRepo repo.Service,
|
||||
grafanaVersion string,
|
||||
@@ -168,6 +168,6 @@ func (c *check) canBeInstalled(ctx context.Context, pluginType string) (bool, er
|
||||
return isAvailableInRepo, nil
|
||||
}
|
||||
|
||||
type pluginContextProvider interface {
|
||||
type PluginContextProvider interface {
|
||||
GetWithDataSource(ctx context.Context, pluginID string, user identity.Requester, ds *datasources.DataSource) (backend.PluginContext, error)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
type healthCheckStep struct {
|
||||
PluginContextProvider pluginContextProvider
|
||||
PluginContextProvider PluginContextProvider
|
||||
PluginClient plugins.Client
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ func main() {
|
||||
KubeConfig: rest.Config{}, // this will be replaced by the apiserver loopback config
|
||||
ManifestData: *apis.LocalManifest().ManifestData,
|
||||
SpecificConfig: checkregistry.AdvisorAppConfig{
|
||||
CheckRegistry: &mockchecks.CheckRegistry{},
|
||||
CheckRegistry: mockchecks.New(),
|
||||
PluginConfig: map[string]string{},
|
||||
StackID: "1", // Numeric stack ID for standalone mode
|
||||
OrgService: nil, // Not needed when StackID is set
|
||||
|
||||
@@ -3,8 +3,9 @@ package dashboard
|
||||
// Information about how the requesting user can use a given dashboard
|
||||
type DashboardAccess struct {
|
||||
// Metadata fields
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
|
||||
// The permissions part
|
||||
CanSave bool `json:"canSave"`
|
||||
|
||||
@@ -12,8 +12,9 @@ type DashboardWithAccessInfo struct {
|
||||
// +k8s:deepcopy-gen=true
|
||||
type DashboardAccess struct {
|
||||
// Metadata fields
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
|
||||
// The permissions part
|
||||
CanSave bool `json:"canSave"`
|
||||
|
||||
@@ -112,6 +112,7 @@ func Convert_dashboard_AnnotationPermission_To_v0alpha1_AnnotationPermission(in
|
||||
func autoConvert_v0alpha1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error {
|
||||
out.Slug = in.Slug
|
||||
out.Url = in.Url
|
||||
out.IsPublic = in.IsPublic
|
||||
out.CanSave = in.CanSave
|
||||
out.CanEdit = in.CanEdit
|
||||
out.CanAdmin = in.CanAdmin
|
||||
@@ -129,6 +130,7 @@ func Convert_v0alpha1_DashboardAccess_To_dashboard_DashboardAccess(in *Dashboard
|
||||
func autoConvert_dashboard_DashboardAccess_To_v0alpha1_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error {
|
||||
out.Slug = in.Slug
|
||||
out.Url = in.Url
|
||||
out.IsPublic = in.IsPublic
|
||||
out.CanSave = in.CanSave
|
||||
out.CanEdit = in.CanEdit
|
||||
out.CanAdmin = in.CanAdmin
|
||||
|
||||
@@ -170,6 +170,13 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardAccess(ref common.ReferenceCall
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"isPublic": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: false,
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"canSave": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The permissions part",
|
||||
@@ -212,7 +219,7 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardAccess(ref common.ReferenceCall
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"},
|
||||
Required: []string{"isPublic", "canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
|
||||
@@ -123,8 +123,9 @@ type DashboardWithAccessInfo struct {
|
||||
// +k8s:deepcopy-gen=true
|
||||
type DashboardAccess struct {
|
||||
// Metadata fields
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
|
||||
// The permissions part
|
||||
CanSave bool `json:"canSave"`
|
||||
|
||||
@@ -118,6 +118,7 @@ func Convert_dashboard_AnnotationPermission_To_v1beta1_AnnotationPermission(in *
|
||||
func autoConvert_v1beta1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error {
|
||||
out.Slug = in.Slug
|
||||
out.Url = in.Url
|
||||
out.IsPublic = in.IsPublic
|
||||
out.CanSave = in.CanSave
|
||||
out.CanEdit = in.CanEdit
|
||||
out.CanAdmin = in.CanAdmin
|
||||
@@ -135,6 +136,7 @@ func Convert_v1beta1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardA
|
||||
func autoConvert_dashboard_DashboardAccess_To_v1beta1_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error {
|
||||
out.Slug = in.Slug
|
||||
out.Url = in.Url
|
||||
out.IsPublic = in.IsPublic
|
||||
out.CanSave = in.CanSave
|
||||
out.CanEdit = in.CanEdit
|
||||
out.CanAdmin = in.CanAdmin
|
||||
|
||||
@@ -165,6 +165,13 @@ func schema_pkg_apis_dashboard_v1beta1_DashboardAccess(ref common.ReferenceCallb
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"isPublic": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: false,
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"canSave": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The permissions part",
|
||||
@@ -207,7 +214,7 @@ func schema_pkg_apis_dashboard_v1beta1_DashboardAccess(ref common.ReferenceCallb
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"},
|
||||
Required: []string{"isPublic", "canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
|
||||
@@ -123,8 +123,9 @@ type DashboardWithAccessInfo struct {
|
||||
// +k8s:deepcopy-gen=true
|
||||
type DashboardAccess struct {
|
||||
// Metadata fields
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
|
||||
// The permissions part
|
||||
CanSave bool `json:"canSave"`
|
||||
|
||||
@@ -118,6 +118,7 @@ func Convert_dashboard_AnnotationPermission_To_v2alpha1_AnnotationPermission(in
|
||||
func autoConvert_v2alpha1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error {
|
||||
out.Slug = in.Slug
|
||||
out.Url = in.Url
|
||||
out.IsPublic = in.IsPublic
|
||||
out.CanSave = in.CanSave
|
||||
out.CanEdit = in.CanEdit
|
||||
out.CanAdmin = in.CanAdmin
|
||||
@@ -135,6 +136,7 @@ func Convert_v2alpha1_DashboardAccess_To_dashboard_DashboardAccess(in *Dashboard
|
||||
func autoConvert_dashboard_DashboardAccess_To_v2alpha1_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error {
|
||||
out.Slug = in.Slug
|
||||
out.Url = in.Url
|
||||
out.IsPublic = in.IsPublic
|
||||
out.CanSave = in.CanSave
|
||||
out.CanEdit = in.CanEdit
|
||||
out.CanAdmin = in.CanAdmin
|
||||
|
||||
@@ -265,6 +265,13 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardAccess(ref common.ReferenceCall
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"isPublic": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: false,
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"canSave": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The permissions part",
|
||||
@@ -307,7 +314,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardAccess(ref common.ReferenceCall
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"},
|
||||
Required: []string{"isPublic", "canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
|
||||
@@ -123,8 +123,9 @@ type DashboardWithAccessInfo struct {
|
||||
// +k8s:deepcopy-gen=true
|
||||
type DashboardAccess struct {
|
||||
// Metadata fields
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
|
||||
// The permissions part
|
||||
CanSave bool `json:"canSave"`
|
||||
|
||||
@@ -118,6 +118,7 @@ func Convert_dashboard_AnnotationPermission_To_v2beta1_AnnotationPermission(in *
|
||||
func autoConvert_v2beta1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardAccess, out *dashboard.DashboardAccess, s conversion.Scope) error {
|
||||
out.Slug = in.Slug
|
||||
out.Url = in.Url
|
||||
out.IsPublic = in.IsPublic
|
||||
out.CanSave = in.CanSave
|
||||
out.CanEdit = in.CanEdit
|
||||
out.CanAdmin = in.CanAdmin
|
||||
@@ -135,6 +136,7 @@ func Convert_v2beta1_DashboardAccess_To_dashboard_DashboardAccess(in *DashboardA
|
||||
func autoConvert_dashboard_DashboardAccess_To_v2beta1_DashboardAccess(in *dashboard.DashboardAccess, out *DashboardAccess, s conversion.Scope) error {
|
||||
out.Slug = in.Slug
|
||||
out.Url = in.Url
|
||||
out.IsPublic = in.IsPublic
|
||||
out.CanSave = in.CanSave
|
||||
out.CanEdit = in.CanEdit
|
||||
out.CanAdmin = in.CanAdmin
|
||||
|
||||
@@ -269,6 +269,13 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAccess(ref common.ReferenceCallb
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"isPublic": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: false,
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"canSave": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The permissions part",
|
||||
@@ -311,7 +318,7 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAccess(ref common.ReferenceCallb
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"},
|
||||
Required: []string{"isPublic", "canSave", "canEdit", "canAdmin", "canStar", "canDelete", "annotationsPermissions"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -122,6 +122,8 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk=
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw=
|
||||
@@ -427,6 +429,10 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
@@ -436,6 +442,8 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
@@ -507,6 +515,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
|
||||
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
|
||||
@@ -595,6 +605,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7BdIXhsA=
|
||||
github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw=
|
||||
github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs=
|
||||
@@ -1106,6 +1118,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/m3db/prometheus_remote_client_golang v0.4.4 h1:DsAIjVKoCp7Ym35tAOFL1OuMLIdIikAEHeNPHY+yyM8=
|
||||
github.com/m3db/prometheus_remote_client_golang v0.4.4/go.mod h1:wHfVbA3eAK6dQvKjCkHhusWYegCk3bDGkA15zymSHdc=
|
||||
github.com/madflojo/testcerts v1.4.0 h1:I09gN0C1ly9IgeVNcAqKk8RAKIJTe3QnFrrPBDyvzN4=
|
||||
@@ -1114,6 +1128,8 @@ github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 h1:hQWBtNqRYrI7CWIaUSXXtNKR90KzcUA5uiuxFVWw7sU=
|
||||
@@ -1197,8 +1213,20 @@ github.com/mithrandie/ternary v1.1.1 h1:k/joD6UGVYxHixYmSR8EGgDFNONBMqyD373xT4QR
|
||||
github.com/mithrandie/ternary v1.1.1/go.mod h1:0D9Ba3+09K2TdSZO7/bFCC0GjSXetCvYuYq0u8FY/1g=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/mocktools/go-smtp-mock/v2 v2.5.1 h1:QcMJMChSgG1olVj4o6xxQFdrWzRjYNrcq660HAjd0wA=
|
||||
github.com/mocktools/go-smtp-mock/v2 v2.5.1/go.mod h1:Rr8M2njlxx//l5INl2+uESnsL2lDsL24teEykCrGfmE=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -1212,6 +1240,8 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWu
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
@@ -1319,6 +1349,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/pressly/goose/v3 v3.25.0 h1:6WeYhMWGRCzpyd89SpODFnCBCKz41KrVbRT58nVjGng=
|
||||
github.com/pressly/goose/v3 v3.25.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
@@ -1419,6 +1451,8 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3 h1:/4/IJi5iyTdh6mqOUaASW148HQpujYiHl0Wl78dSOSc=
|
||||
github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3/go.mod h1:aJIMhRsunltJR926EB2MUg8qHemFQDreSB33pyto2Ps=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
@@ -1498,6 +1532,8 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
|
||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/testcontainers/testcontainers-go v0.36.0 h1:YpffyLuHtdp5EUsI5mT4sRw8GZhO/5ozyDT1xWGXt00=
|
||||
github.com/testcontainers/testcontainers-go v0.36.0/go.mod h1:yk73GVJ0KUZIHUtFna6MO7QS144qYpoY8lEEtU9Hed0=
|
||||
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
|
||||
github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
github.com/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM=
|
||||
@@ -1507,6 +1543,10 @@ github.com/thomaspoignant/go-feature-flag v1.42.0/go.mod h1:y0QiWH7chHWhGATb/+Xq
|
||||
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o=
|
||||
github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
@@ -1562,6 +1602,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
|
||||
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
|
||||
@@ -6,6 +6,7 @@ require (
|
||||
github.com/grafana/grafana-app-sdk v0.48.1
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.1
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250428110029-a8ea72012bde
|
||||
github.com/stretchr/testify v1.11.1
|
||||
k8s.io/apimachinery v0.34.1
|
||||
k8s.io/apiserver v0.34.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
|
||||
@@ -93,6 +93,7 @@ func equalStringPointers(a, b *string) bool {
|
||||
type InstallRegistrar struct {
|
||||
clientGenerator resource.ClientGenerator
|
||||
client *pluginsv0alpha1.PluginClient
|
||||
clientErr error
|
||||
clientOnce sync.Once
|
||||
}
|
||||
|
||||
@@ -107,20 +108,21 @@ func (r *InstallRegistrar) GetClient() (*pluginsv0alpha1.PluginClient, error) {
|
||||
r.clientOnce.Do(func() {
|
||||
client, err := pluginsv0alpha1.NewPluginClientFromGenerator(r.clientGenerator)
|
||||
if err != nil {
|
||||
r.clientErr = err
|
||||
r.client = nil
|
||||
return
|
||||
}
|
||||
r.client = client
|
||||
})
|
||||
|
||||
return r.client, nil
|
||||
return r.client, r.clientErr
|
||||
}
|
||||
|
||||
// Register creates or updates a plugin install in the registry.
|
||||
func (r *InstallRegistrar) Register(ctx context.Context, namespace string, install *PluginInstall) error {
|
||||
client, err := r.GetClient()
|
||||
if err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
identifier := resource.Identifier{
|
||||
Namespace: namespace,
|
||||
@@ -132,9 +134,12 @@ func (r *InstallRegistrar) Register(ctx context.Context, namespace string, insta
|
||||
return err
|
||||
}
|
||||
|
||||
if existing != nil && install.ShouldUpdate(existing) {
|
||||
_, err = client.Update(ctx, install.ToPluginInstallV0Alpha1(namespace), resource.UpdateOptions{ResourceVersion: existing.ResourceVersion})
|
||||
return err
|
||||
if existing != nil {
|
||||
if install.ShouldUpdate(existing) {
|
||||
_, err = client.Update(ctx, install.ToPluginInstallV0Alpha1(namespace), resource.UpdateOptions{ResourceVersion: existing.ResourceVersion})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = client.Create(ctx, install.ToPluginInstallV0Alpha1(namespace), resource.CreateOptions{})
|
||||
@@ -155,6 +160,10 @@ func (r *InstallRegistrar) Unregister(ctx context.Context, namespace string, nam
|
||||
if err != nil && !errorsK8s.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
// if the plugin doesn't exist, nothing to unregister
|
||||
if existing == nil {
|
||||
return nil
|
||||
}
|
||||
// if the source is different, do not unregister
|
||||
if existingSource, ok := existing.Annotations[PluginInstallSourceAnnotation]; ok && existingSource != source {
|
||||
return nil
|
||||
|
||||
908
apps/plugins/pkg/app/install/registrar_test.go
Normal file
908
apps/plugins/pkg/app/install/registrar_test.go
Normal file
@@ -0,0 +1,908 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
"github.com/stretchr/testify/require"
|
||||
errorsK8s "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
)
|
||||
|
||||
func TestPluginInstall_ShouldUpdate(t *testing.T) {
|
||||
baseExisting := &pluginsv0alpha1.Plugin{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-1",
|
||||
Name: "plugin-1",
|
||||
Annotations: map[string]string{
|
||||
PluginInstallSourceAnnotation: SourcePluginStore,
|
||||
},
|
||||
},
|
||||
Spec: pluginsv0alpha1.PluginSpec{
|
||||
Id: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
Class: pluginsv0alpha1.PluginSpecClass(ClassExternal),
|
||||
},
|
||||
}
|
||||
|
||||
baseInstall := PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
modifyInstall func(*PluginInstall)
|
||||
modifyExisting func(*pluginsv0alpha1.Plugin)
|
||||
expectUpdate bool
|
||||
}{
|
||||
{
|
||||
name: "no changes",
|
||||
expectUpdate: false,
|
||||
},
|
||||
{
|
||||
name: "version differs",
|
||||
modifyInstall: func(pi *PluginInstall) {
|
||||
pi.Version = "2.0.0"
|
||||
},
|
||||
expectUpdate: true,
|
||||
},
|
||||
{
|
||||
name: "class differs",
|
||||
modifyInstall: func(pi *PluginInstall) {
|
||||
pi.Class = ClassCore
|
||||
},
|
||||
expectUpdate: true,
|
||||
},
|
||||
{
|
||||
name: "url differs",
|
||||
modifyInstall: func(pi *PluginInstall) {
|
||||
pi.URL = "https://example.com/plugin.zip"
|
||||
},
|
||||
expectUpdate: true,
|
||||
},
|
||||
{
|
||||
name: "source differs",
|
||||
modifyExisting: func(existing *pluginsv0alpha1.Plugin) {
|
||||
existing.Annotations[PluginInstallSourceAnnotation] = SourceUnknown
|
||||
},
|
||||
expectUpdate: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
existing := baseExisting.DeepCopy()
|
||||
install := baseInstall
|
||||
|
||||
if tt.modifyExisting != nil {
|
||||
tt.modifyExisting(existing)
|
||||
}
|
||||
if tt.modifyInstall != nil {
|
||||
tt.modifyInstall(&install)
|
||||
}
|
||||
|
||||
require.Equal(t, tt.expectUpdate, install.ShouldUpdate(existing))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallRegistrar_Register(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
install *PluginInstall
|
||||
existing *pluginsv0alpha1.Plugin
|
||||
existingErr error
|
||||
expectedCreates int
|
||||
expectedUpdates int
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "creates plugin when not found",
|
||||
install: &PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
existingErr: errorsK8s.NewNotFound(pluginGroupResource(), "plugin-1"),
|
||||
expectedCreates: 1,
|
||||
},
|
||||
{
|
||||
name: "updates plugin when fields change",
|
||||
install: &PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "2.0.0",
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
existing: &pluginsv0alpha1.Plugin{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-1",
|
||||
Name: "plugin-1",
|
||||
ResourceVersion: "7",
|
||||
Annotations: map[string]string{
|
||||
PluginInstallSourceAnnotation: SourcePluginStore,
|
||||
},
|
||||
},
|
||||
Spec: pluginsv0alpha1.PluginSpec{
|
||||
Id: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
Class: pluginsv0alpha1.PluginSpecClass(ClassExternal),
|
||||
},
|
||||
},
|
||||
expectedUpdates: 1,
|
||||
},
|
||||
{
|
||||
name: "skips create when plugin matches",
|
||||
install: &PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
existing: &pluginsv0alpha1.Plugin{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-1",
|
||||
Name: "plugin-1",
|
||||
ResourceVersion: "9",
|
||||
Annotations: map[string]string{
|
||||
PluginInstallSourceAnnotation: SourcePluginStore,
|
||||
},
|
||||
},
|
||||
Spec: pluginsv0alpha1.PluginSpec{
|
||||
Id: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
Class: pluginsv0alpha1.PluginSpecClass(ClassExternal),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "returns error on unexpected get failure",
|
||||
install: &PluginInstall{
|
||||
ID: "plugin-err",
|
||||
Version: "1.0.0",
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
existingErr: errorsK8s.NewInternalError(errors.New("boom")),
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
createCalls := 0
|
||||
updateCalls := 0
|
||||
var receivedResourceVersions []string
|
||||
var updatedPlugins []*pluginsv0alpha1.Plugin
|
||||
|
||||
fakeClient := &fakePluginInstallClient{
|
||||
getFunc: func(context.Context, resource.Identifier) (*pluginsv0alpha1.Plugin, error) {
|
||||
if tt.existingErr != nil {
|
||||
return nil, tt.existingErr
|
||||
}
|
||||
if tt.existing == nil {
|
||||
return nil, errorsK8s.NewNotFound(pluginGroupResource(), "plugin-1")
|
||||
}
|
||||
return tt.existing.DeepCopy(), nil
|
||||
},
|
||||
createFunc: func(context.Context, *pluginsv0alpha1.Plugin, resource.CreateOptions) (*pluginsv0alpha1.Plugin, error) {
|
||||
createCalls++
|
||||
return tt.install.ToPluginInstallV0Alpha1("org-1"), nil
|
||||
},
|
||||
updateFunc: func(_ context.Context, obj *pluginsv0alpha1.Plugin, opts resource.UpdateOptions) (*pluginsv0alpha1.Plugin, error) {
|
||||
updateCalls++
|
||||
receivedResourceVersions = append(receivedResourceVersions, opts.ResourceVersion)
|
||||
updatedPlugins = append(updatedPlugins, obj)
|
||||
return obj, nil
|
||||
},
|
||||
}
|
||||
|
||||
registrar := NewInstallRegistrar(&fakeClientGenerator{client: fakeClient})
|
||||
|
||||
err := registrar.Register(ctx, "org-1", tt.install)
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expectedCreates, createCalls)
|
||||
require.Equal(t, tt.expectedUpdates, updateCalls)
|
||||
|
||||
if tt.expectedUpdates > 0 {
|
||||
require.Equal(t, []string{tt.existing.ResourceVersion}, receivedResourceVersions)
|
||||
require.Len(t, updatedPlugins, 1)
|
||||
require.Equal(t, tt.install.Version, updatedPlugins[0].Spec.Version)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func pluginGroupResource() schema.GroupResource {
|
||||
return schema.GroupResource{Group: pluginsv0alpha1.APIGroup, Resource: "plugininstalls"}
|
||||
}
|
||||
|
||||
type fakePluginInstallClient struct {
|
||||
listAllFunc func(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginList, error)
|
||||
getFunc func(ctx context.Context, identifier resource.Identifier) (*pluginsv0alpha1.Plugin, error)
|
||||
createFunc func(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.CreateOptions) (*pluginsv0alpha1.Plugin, error)
|
||||
updateFunc func(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.UpdateOptions) (*pluginsv0alpha1.Plugin, error)
|
||||
deleteFunc func(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error
|
||||
}
|
||||
|
||||
func (f *fakePluginInstallClient) Get(ctx context.Context, identifier resource.Identifier) (*pluginsv0alpha1.Plugin, error) {
|
||||
if f.getFunc != nil {
|
||||
return f.getFunc(ctx, identifier)
|
||||
}
|
||||
return nil, errorsK8s.NewNotFound(pluginGroupResource(), identifier.Name)
|
||||
}
|
||||
|
||||
func (f *fakePluginInstallClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginList, error) {
|
||||
if f.listAllFunc != nil {
|
||||
return f.listAllFunc(ctx, namespace, opts)
|
||||
}
|
||||
return &pluginsv0alpha1.PluginList{}, nil
|
||||
}
|
||||
|
||||
func (f *fakePluginInstallClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginList, error) {
|
||||
return f.ListAll(ctx, namespace, opts)
|
||||
}
|
||||
|
||||
func (f *fakePluginInstallClient) Create(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.CreateOptions) (*pluginsv0alpha1.Plugin, error) {
|
||||
if f.createFunc != nil {
|
||||
return f.createFunc(ctx, obj, opts)
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (f *fakePluginInstallClient) Update(ctx context.Context, obj *pluginsv0alpha1.Plugin, opts resource.UpdateOptions) (*pluginsv0alpha1.Plugin, error) {
|
||||
if f.updateFunc != nil {
|
||||
return f.updateFunc(ctx, obj, opts)
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (f *fakePluginInstallClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus pluginsv0alpha1.PluginStatus, opts resource.UpdateOptions) (*pluginsv0alpha1.Plugin, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakePluginInstallClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*pluginsv0alpha1.Plugin, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakePluginInstallClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
|
||||
if f.deleteFunc != nil {
|
||||
return f.deleteFunc(ctx, identifier, opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeClientGenerator struct {
|
||||
client *fakePluginInstallClient
|
||||
shouldError bool
|
||||
}
|
||||
|
||||
func (f *fakeClientGenerator) ClientFor(resource.Kind) (resource.Client, error) {
|
||||
if f.shouldError {
|
||||
return nil, errors.New("client generation failed")
|
||||
}
|
||||
return &fakeResourceClient{client: f.client}, nil
|
||||
}
|
||||
|
||||
type fakeResourceClient struct {
|
||||
client *fakePluginInstallClient
|
||||
}
|
||||
|
||||
func (f *fakeResourceClient) Get(ctx context.Context, identifier resource.Identifier) (resource.Object, error) {
|
||||
return f.client.Get(ctx, identifier)
|
||||
}
|
||||
|
||||
func (f *fakeResourceClient) GetInto(ctx context.Context, identifier resource.Identifier, into resource.Object) error {
|
||||
obj, err := f.client.Get(ctx, identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if target, ok := into.(*pluginsv0alpha1.Plugin); ok {
|
||||
*target = *obj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeResourceClient) List(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) {
|
||||
return f.client.ListAll(ctx, namespace, options)
|
||||
}
|
||||
|
||||
func (f *fakeResourceClient) ListInto(ctx context.Context, namespace string, options resource.ListOptions, into resource.ListObject) error {
|
||||
list, err := f.client.ListAll(ctx, namespace, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if target, ok := into.(*pluginsv0alpha1.PluginList); ok {
|
||||
*target = *list
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeResourceClient) Create(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions) (resource.Object, error) {
|
||||
plugin := obj.(*pluginsv0alpha1.Plugin)
|
||||
return f.client.Create(ctx, plugin, options)
|
||||
}
|
||||
|
||||
func (f *fakeResourceClient) CreateInto(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions, into resource.Object) error {
|
||||
created, err := f.Create(ctx, identifier, obj, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if plugin, ok := created.(*pluginsv0alpha1.Plugin); ok {
|
||||
if target, ok := into.(*pluginsv0alpha1.Plugin); ok {
|
||||
*target = *plugin
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeResourceClient) Update(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.UpdateOptions) (resource.Object, error) {
|
||||
plugin := obj.(*pluginsv0alpha1.Plugin)
|
||||
return f.client.Update(ctx, plugin, options)
|
||||
}
|
||||
|
||||
func (f *fakeResourceClient) UpdateInto(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.UpdateOptions, into resource.Object) error {
|
||||
updated, err := f.Update(ctx, identifier, obj, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if plugin, ok := updated.(*pluginsv0alpha1.Plugin); ok {
|
||||
if target, ok := into.(*pluginsv0alpha1.Plugin); ok {
|
||||
*target = *plugin
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeResourceClient) Patch(ctx context.Context, identifier resource.Identifier, patch resource.PatchRequest, options resource.PatchOptions) (resource.Object, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeResourceClient) PatchInto(ctx context.Context, identifier resource.Identifier, patch resource.PatchRequest, options resource.PatchOptions, into resource.Object) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeResourceClient) Delete(ctx context.Context, identifier resource.Identifier, options resource.DeleteOptions) error {
|
||||
return f.client.Delete(ctx, identifier, options)
|
||||
}
|
||||
|
||||
func (f *fakeResourceClient) SubresourceRequest(ctx context.Context, identifier resource.Identifier, req resource.CustomRouteRequestOptions) ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeResourceClient) Watch(ctx context.Context, namespace string, options resource.WatchOptions) (resource.WatchResponse, error) {
|
||||
return &fakeWatchResponse{}, nil
|
||||
}
|
||||
|
||||
type fakeWatchResponse struct{}
|
||||
|
||||
func (f *fakeWatchResponse) Stop() {}
|
||||
|
||||
func (f *fakeWatchResponse) WatchEvents() <-chan resource.WatchEvent {
|
||||
ch := make(chan resource.WatchEvent)
|
||||
close(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
install PluginInstall
|
||||
namespace string
|
||||
validate func(*testing.T, *pluginsv0alpha1.Plugin)
|
||||
}{
|
||||
{
|
||||
name: "empty URL creates nil pointer",
|
||||
install: PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
namespace: "org-1",
|
||||
validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) {
|
||||
require.Nil(t, p.Spec.Url)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-empty URL creates pointer",
|
||||
install: PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
URL: "https://example.com/plugin.zip",
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
namespace: "org-1",
|
||||
validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) {
|
||||
require.NotNil(t, p.Spec.Url)
|
||||
require.Equal(t, "https://example.com/plugin.zip", *p.Spec.Url)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "core class is mapped correctly",
|
||||
install: PluginInstall{
|
||||
ID: "plugin-core",
|
||||
Version: "2.0.0",
|
||||
Class: ClassCore,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
namespace: "org-2",
|
||||
validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) {
|
||||
require.Equal(t, pluginsv0alpha1.PluginSpecClass(ClassCore), p.Spec.Class)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cdn class is mapped correctly",
|
||||
install: PluginInstall{
|
||||
ID: "plugin-cdn",
|
||||
Version: "3.0.0",
|
||||
Class: ClassCDN,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
namespace: "org-3",
|
||||
validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) {
|
||||
require.Equal(t, pluginsv0alpha1.PluginSpecClass(ClassCDN), p.Spec.Class)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "source annotation is set correctly",
|
||||
install: PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
Class: ClassExternal,
|
||||
Source: SourceUnknown,
|
||||
},
|
||||
namespace: "org-1",
|
||||
validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) {
|
||||
require.Equal(t, SourceUnknown, p.Annotations[PluginInstallSourceAnnotation])
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "namespace and name are set correctly",
|
||||
install: PluginInstall{
|
||||
ID: "my-plugin",
|
||||
Version: "1.0.0",
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
namespace: "my-namespace",
|
||||
validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) {
|
||||
require.Equal(t, "my-namespace", p.Namespace)
|
||||
require.Equal(t, "my-plugin", p.Name)
|
||||
require.Equal(t, "my-plugin", p.Spec.Id)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.install.ToPluginInstallV0Alpha1(tt.namespace)
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, tt.namespace, result.Namespace)
|
||||
require.Equal(t, tt.install.ID, result.Name)
|
||||
require.Equal(t, tt.install.ID, result.Spec.Id)
|
||||
require.Equal(t, tt.install.Version, result.Spec.Version)
|
||||
tt.validate(t, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualStringPointers(t *testing.T) {
|
||||
str1 := "value1"
|
||||
str2 := "value2"
|
||||
str3 := "value1"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a *string
|
||||
b *string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "both nil",
|
||||
a: nil,
|
||||
b: nil,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "first nil, second non-nil",
|
||||
a: nil,
|
||||
b: &str1,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "first non-nil, second nil",
|
||||
a: &str1,
|
||||
b: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "both non-nil with same value",
|
||||
a: &str1,
|
||||
b: &str3,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "both non-nil with different values",
|
||||
a: &str1,
|
||||
b: &str2,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := equalStringPointers(tt.a, tt.b)
|
||||
require.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) {
|
||||
existingURL := "https://old.example.com/plugin.zip"
|
||||
newURL := "https://new.example.com/plugin.zip"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
install PluginInstall
|
||||
existingURL *string
|
||||
expectUpdate bool
|
||||
}{
|
||||
{
|
||||
name: "URL transition from nil to non-nil",
|
||||
install: PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
URL: newURL,
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
existingURL: nil,
|
||||
expectUpdate: true,
|
||||
},
|
||||
{
|
||||
name: "URL transition from non-nil to nil",
|
||||
install: PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
URL: "",
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
existingURL: &existingURL,
|
||||
expectUpdate: true,
|
||||
},
|
||||
{
|
||||
name: "URL stays nil",
|
||||
install: PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
URL: "",
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
existingURL: nil,
|
||||
expectUpdate: false,
|
||||
},
|
||||
{
|
||||
name: "URL stays same non-nil value",
|
||||
install: PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
URL: existingURL,
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
existingURL: &existingURL,
|
||||
expectUpdate: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
existing := &pluginsv0alpha1.Plugin{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-1",
|
||||
Name: "plugin-1",
|
||||
Annotations: map[string]string{
|
||||
PluginInstallSourceAnnotation: SourcePluginStore,
|
||||
},
|
||||
},
|
||||
Spec: pluginsv0alpha1.PluginSpec{
|
||||
Id: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
Url: tt.existingURL,
|
||||
Class: pluginsv0alpha1.PluginSpecClass(ClassExternal),
|
||||
},
|
||||
}
|
||||
|
||||
require.Equal(t, tt.expectUpdate, tt.install.ShouldUpdate(existing))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallRegistrar_GetClient(t *testing.T) {
|
||||
t.Run("successfully creates client on first call", func(t *testing.T) {
|
||||
fakeClient := &fakePluginInstallClient{}
|
||||
generator := &fakeClientGenerator{client: fakeClient}
|
||||
registrar := NewInstallRegistrar(generator)
|
||||
|
||||
client, err := registrar.GetClient()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
})
|
||||
|
||||
t.Run("returns same client on subsequent calls", func(t *testing.T) {
|
||||
fakeClient := &fakePluginInstallClient{}
|
||||
generator := &fakeClientGenerator{client: fakeClient}
|
||||
registrar := NewInstallRegistrar(generator)
|
||||
|
||||
client1, err1 := registrar.GetClient()
|
||||
require.NoError(t, err1)
|
||||
|
||||
client2, err2 := registrar.GetClient()
|
||||
require.NoError(t, err2)
|
||||
|
||||
require.Equal(t, client1, client2)
|
||||
})
|
||||
|
||||
t.Run("returns error when client generation fails", func(t *testing.T) {
|
||||
generator := &fakeClientGenerator{client: nil, shouldError: true}
|
||||
registrar := NewInstallRegistrar(generator)
|
||||
|
||||
client, err := registrar.GetClient()
|
||||
require.Error(t, err)
|
||||
require.Nil(t, client)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstallRegistrar_Register_ErrorCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
install *PluginInstall
|
||||
setupClient func(*fakePluginInstallClient)
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "create fails",
|
||||
install: &PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
setupClient: func(fc *fakePluginInstallClient) {
|
||||
fc.getFunc = func(context.Context, resource.Identifier) (*pluginsv0alpha1.Plugin, error) {
|
||||
return nil, errorsK8s.NewNotFound(pluginGroupResource(), "plugin-1")
|
||||
}
|
||||
fc.createFunc = func(context.Context, *pluginsv0alpha1.Plugin, resource.CreateOptions) (*pluginsv0alpha1.Plugin, error) {
|
||||
return nil, errors.New("create failed")
|
||||
}
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "update fails",
|
||||
install: &PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "2.0.0",
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
},
|
||||
setupClient: func(fc *fakePluginInstallClient) {
|
||||
fc.getFunc = func(context.Context, resource.Identifier) (*pluginsv0alpha1.Plugin, error) {
|
||||
return &pluginsv0alpha1.Plugin{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-1",
|
||||
Name: "plugin-1",
|
||||
ResourceVersion: "5",
|
||||
Annotations: map[string]string{
|
||||
PluginInstallSourceAnnotation: SourcePluginStore,
|
||||
},
|
||||
},
|
||||
Spec: pluginsv0alpha1.PluginSpec{
|
||||
Id: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
Class: pluginsv0alpha1.PluginSpecClass(ClassExternal),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
fc.updateFunc = func(context.Context, *pluginsv0alpha1.Plugin, resource.UpdateOptions) (*pluginsv0alpha1.Plugin, error) {
|
||||
return nil, errors.New("update failed")
|
||||
}
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fakeClient := &fakePluginInstallClient{}
|
||||
tt.setupClient(fakeClient)
|
||||
|
||||
registrar := NewInstallRegistrar(&fakeClientGenerator{client: fakeClient})
|
||||
|
||||
err := registrar.Register(ctx, "org-1", tt.install)
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallRegistrar_Unregister(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
pluginName string
|
||||
source Source
|
||||
existing *pluginsv0alpha1.Plugin
|
||||
existingErr error
|
||||
expectedCalls int
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "successfully deletes plugin with matching source",
|
||||
namespace: "org-1",
|
||||
pluginName: "plugin-1",
|
||||
source: SourcePluginStore,
|
||||
existing: &pluginsv0alpha1.Plugin{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-1",
|
||||
Name: "plugin-1",
|
||||
Annotations: map[string]string{
|
||||
PluginInstallSourceAnnotation: SourcePluginStore,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "plugin not found should not error",
|
||||
namespace: "org-1",
|
||||
pluginName: "plugin-nonexistent",
|
||||
source: SourcePluginStore,
|
||||
existingErr: errorsK8s.NewNotFound(pluginGroupResource(), "plugin-nonexistent"),
|
||||
expectedCalls: 0,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "skips delete when source doesn't match",
|
||||
namespace: "org-1",
|
||||
pluginName: "plugin-1",
|
||||
source: SourcePluginStore,
|
||||
existing: &pluginsv0alpha1.Plugin{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-1",
|
||||
Name: "plugin-1",
|
||||
Annotations: map[string]string{
|
||||
PluginInstallSourceAnnotation: SourceUnknown,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedCalls: 0,
|
||||
},
|
||||
{
|
||||
name: "returns error on unexpected get failure",
|
||||
namespace: "org-1",
|
||||
pluginName: "plugin-err",
|
||||
source: SourcePluginStore,
|
||||
existingErr: errorsK8s.NewInternalError(errors.New("get failed")),
|
||||
expectedCalls: 0,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "delete failure returns error",
|
||||
namespace: "org-1",
|
||||
pluginName: "plugin-1",
|
||||
source: SourcePluginStore,
|
||||
existing: &pluginsv0alpha1.Plugin{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-1",
|
||||
Name: "plugin-1",
|
||||
Annotations: map[string]string{
|
||||
PluginInstallSourceAnnotation: SourcePluginStore,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedCalls: 1,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "handles missing source annotation",
|
||||
namespace: "org-1",
|
||||
pluginName: "plugin-1",
|
||||
source: SourcePluginStore,
|
||||
existing: &pluginsv0alpha1.Plugin{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "org-1",
|
||||
Name: "plugin-1",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
expectedCalls: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
deleteCalls := 0
|
||||
|
||||
fakeClient := &fakePluginInstallClient{
|
||||
getFunc: func(context.Context, resource.Identifier) (*pluginsv0alpha1.Plugin, error) {
|
||||
if tt.existingErr != nil {
|
||||
return nil, tt.existingErr
|
||||
}
|
||||
if tt.existing == nil {
|
||||
return nil, errorsK8s.NewNotFound(pluginGroupResource(), tt.pluginName)
|
||||
}
|
||||
return tt.existing.DeepCopy(), nil
|
||||
},
|
||||
deleteFunc: func(context.Context, resource.Identifier, resource.DeleteOptions) error {
|
||||
deleteCalls++
|
||||
if tt.name == "delete failure returns error" {
|
||||
return errors.New("delete failed")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
registrar := NewInstallRegistrar(&fakeClientGenerator{client: fakeClient})
|
||||
|
||||
err := registrar.Unregister(ctx, tt.namespace, tt.pluginName, tt.source)
|
||||
|
||||
require.Equal(t, tt.expectedCalls, deleteCalls)
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallRegistrar_GetClientError(t *testing.T) {
|
||||
t.Run("Register returns error with nil client", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
generator := &fakeClientGenerator{client: nil, shouldError: true}
|
||||
registrar := NewInstallRegistrar(generator)
|
||||
|
||||
install := &PluginInstall{
|
||||
ID: "plugin-1",
|
||||
Version: "1.0.0",
|
||||
Class: ClassExternal,
|
||||
Source: SourcePluginStore,
|
||||
}
|
||||
|
||||
err := registrar.Register(ctx, "org-1", install)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Unregister returns error with nil client", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
generator := &fakeClientGenerator{client: nil, shouldError: true}
|
||||
registrar := NewInstallRegistrar(generator)
|
||||
|
||||
err := registrar.Unregister(ctx, "org-1", "plugin-1", SourcePluginStore)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -86,7 +86,7 @@ services:
|
||||
- 'alloy.logs=true'
|
||||
|
||||
alloy:
|
||||
image: grafana/alloy:latest
|
||||
image: grafana/alloy:v1.11.2
|
||||
volumes:
|
||||
- ./configs/alloy:/alloy-config
|
||||
- /var/run/docker.sock:/var/run/docker.sock # To scrape Docker container logs
|
||||
@@ -104,7 +104,7 @@ services:
|
||||
- 'alloy.logs=true'
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
image: prom/prometheus:v3.7.2
|
||||
volumes:
|
||||
- prometheus-data:/prometheus
|
||||
command:
|
||||
@@ -116,7 +116,7 @@ services:
|
||||
- 'alloy.logs=true'
|
||||
|
||||
loki:
|
||||
image: grafana/loki
|
||||
image: grafana/loki:3.5.7
|
||||
volumes:
|
||||
- loki-data:/loki
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
@@ -124,7 +124,7 @@ services:
|
||||
- 'alloy.logs=true'
|
||||
|
||||
tempo-init:
|
||||
image: busybox
|
||||
image: busybox:1.37.0
|
||||
user: root
|
||||
entrypoint:
|
||||
- 'chown'
|
||||
@@ -134,7 +134,7 @@ services:
|
||||
- tempo-data:/var/tempo
|
||||
|
||||
tempo:
|
||||
image: grafana/tempo
|
||||
image: grafana/tempo:2.9.0
|
||||
volumes:
|
||||
- tempo-data:/var/lib/tempo
|
||||
- ./configs/tempo.yaml:/etc/tempo/tempo.yaml
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
"rows-to-fields": (import '../dev-dashboards/transforms/rows-to-fields.json'),
|
||||
"shared_queries": (import '../dev-dashboards/panel-common/shared_queries.json'),
|
||||
"slow_queries_and_annotations": (import '../dev-dashboards/scenarios/slow_queries_and_annotations.json'),
|
||||
"status-history-thresholds-mappings": (import '../dev-dashboards/panel-status-history/status-history-thresholds-mappings.json'),
|
||||
"table_footer": (import '../dev-dashboards/panel-table/table_footer.json'),
|
||||
"table_kitchen_sink": (import '../dev-dashboards/panel-table/table_kitchen_sink.json'),
|
||||
"table_markdown": (import '../dev-dashboards/panel-table/table_markdown.json'),
|
||||
|
||||
@@ -141,6 +141,20 @@ Alternatively, you can use the `index()` function to retrieve the query value:
|
||||
{{ index $values "B" }} CPU usage for {{ index $labels "instance" }} over the last 5 minutes.
|
||||
```
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
|
||||
Variable names that start with a number (for example, `1B`) are not [valid identifiers in Go templates](https://go.dev/ref/spec#Identifiers).
|
||||
|
||||
To access a value or label whose key starts with a number, use the `index` function:
|
||||
|
||||
```
|
||||
{{ index $values "1B" }} CPU usage for {{ index $labels "1instance" }} over the last 5 minutes.
|
||||
```
|
||||
|
||||
Using `{{ $values.1B.Value }}` is invalid and causes the template code to render as plain text.
|
||||
|
||||
{{< /admonition >}}
|
||||
|
||||
#### $value
|
||||
|
||||
The `$value` variable is a string containing the labels and values of all instant queries; threshold, reduce and math expressions, and classic conditions in the alert rule.
|
||||
|
||||
@@ -10,6 +10,12 @@ labels:
|
||||
- enterprise
|
||||
- oss
|
||||
- cloud
|
||||
refs:
|
||||
roles-and-permissions:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/account-management/authentication-and-permissions/cloud-roles/
|
||||
title: Git Sync
|
||||
weight: 100
|
||||
---
|
||||
@@ -84,6 +90,11 @@ Refer to [Requirements](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/obser
|
||||
|
||||
- You can only authenticate in GitHub using your Personal Access Token token.
|
||||
|
||||
**Permission management**
|
||||
|
||||
- You cannot modify the permissions of a provisioned folder after you've synced it.
|
||||
- Default permissions are: Admin = Admin, Editor = Editor, and Viewer = Viewer. Refer to [Roles and permissions](ref:roles-and-permissions) for more information.
|
||||
|
||||
**Compatibility**
|
||||
|
||||
- Support for native Git, Git app, and other providers, such as GitLab or Bitbucket, is on the roadmap.
|
||||
|
||||
87
e2e-playwright/panels-suite/status-history.spec.ts
Normal file
87
e2e-playwright/panels-suite/status-history.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
const DASHBOARD_UID = 'a2f4ad9e-3b44-4624-8067-35f31be5d309';
|
||||
|
||||
test.use({
|
||||
viewport: { width: 1280, height: 2000 },
|
||||
});
|
||||
|
||||
test.describe('Panels test: StatusHistory', { tag: ['@panels', '@status-history'] }, () => {
|
||||
test('renders successfully', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
const dashboardPage = await gotoDashboardPage({
|
||||
uid: DASHBOARD_UID,
|
||||
});
|
||||
|
||||
// check that gauges are rendered
|
||||
const statusHistoryUplot = page.locator('.uplot');
|
||||
await expect(statusHistoryUplot, 'panels are rendered').toHaveCount(11);
|
||||
|
||||
// check that no panel errors exist
|
||||
const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error'));
|
||||
await expect(errorInfo, 'no errors in the panels').toBeHidden();
|
||||
});
|
||||
|
||||
test('"no data"', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
const dashboardPage = await gotoDashboardPage({
|
||||
uid: DASHBOARD_UID,
|
||||
queryParams: new URLSearchParams({ editPanel: '15' }),
|
||||
});
|
||||
|
||||
const statusHistoryUplot = page.locator('.uplot');
|
||||
await expect(statusHistoryUplot, "that uplot doesn't appear").toBeHidden();
|
||||
|
||||
const emptyMessage = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage);
|
||||
await expect(emptyMessage, 'that the empty text appears').toHaveText('No data');
|
||||
|
||||
// update the "No value" option and see if the panel updates
|
||||
const noValueOption = dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Standard options No value'))
|
||||
.locator('input');
|
||||
|
||||
await noValueOption.fill('My empty value');
|
||||
await noValueOption.blur();
|
||||
await expect(emptyMessage, 'that the empty text has changed').toHaveText('My empty value');
|
||||
});
|
||||
|
||||
test('tooltip interactions', async ({ gotoDashboardPage, page, selectors }) => {
|
||||
const dashboardPage = await gotoDashboardPage({
|
||||
uid: DASHBOARD_UID,
|
||||
queryParams: new URLSearchParams({ editPanel: '13' }),
|
||||
});
|
||||
|
||||
const statusHistoryUplot = page.locator('.uplot');
|
||||
await expect(statusHistoryUplot, 'uplot is rendered').toBeVisible();
|
||||
|
||||
const tooltip = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Tooltip.Wrapper);
|
||||
|
||||
// hover over a spot to trigger the tooltip
|
||||
await statusHistoryUplot.hover({ position: { x: 100, y: 50 } });
|
||||
await expect(tooltip, 'tooltip appears on hover').toBeVisible();
|
||||
await expect(tooltip, 'tooltip displays the value').toContainText('value5');
|
||||
|
||||
// click to pin the tooltip, hover away to be sure it's pinned
|
||||
await statusHistoryUplot.click({ position: { x: 100, y: 50 } });
|
||||
await statusHistoryUplot.hover({ position: { x: 300, y: 50 } });
|
||||
await expect(tooltip, 'tooltip pinned on click').toBeVisible();
|
||||
await expect(tooltip, 'tooltip displays the first value').toContainText('value5');
|
||||
|
||||
// unpin the tooltip, ensure it closes on hover away
|
||||
await statusHistoryUplot.click({ position: { x: 300, y: 50 } });
|
||||
await statusHistoryUplot.blur();
|
||||
await expect(tooltip, 'tooltip closed after unpinning and hovering away').toBeHidden();
|
||||
|
||||
// test clicking the "x" as well
|
||||
await statusHistoryUplot.click({ position: { x: 100, y: 50 } });
|
||||
await expect(tooltip, 'tooltip appears on click').toBeVisible();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Portal.container).getByLabel('Close').click();
|
||||
await expect(tooltip, 'tooltip closed on "x" click').toBeHidden();
|
||||
|
||||
// disable tooltips
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Tooltip Tooltip mode'))
|
||||
.getByLabel('Hidden')
|
||||
.click();
|
||||
await statusHistoryUplot.hover({ position: { x: 100, y: 50 } });
|
||||
await expect(tooltip, 'tooltip is not shown when disabled').toBeHidden();
|
||||
});
|
||||
});
|
||||
26
go.mod
26
go.mod
@@ -110,7 +110,6 @@ require (
|
||||
github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // @grafana/observability-traces-and-profiling
|
||||
github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae // @grafana/observability-traces-and-profiling
|
||||
github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec // @grafana/observability-traces-and-profiling
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/grafana-search-and-storage
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // @grafana/plugins-platform-backend
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // @grafana/grafana-backend-group
|
||||
@@ -171,6 +170,7 @@ require (
|
||||
github.com/spf13/pflag v1.0.10 // @grafana-app-platform-squad
|
||||
github.com/spyzhov/ajson v0.9.6 // @grafana/grafana-sharing-squad
|
||||
github.com/stretchr/testify v1.11.1 // @grafana/grafana-backend-group
|
||||
github.com/testcontainers/testcontainers-go v0.36.0 //@grafana/grafana-app-platform-squad
|
||||
github.com/thomaspoignant/go-feature-flag v1.42.0 // @grafana/grafana-backend-group
|
||||
github.com/tjhop/slog-gokit v0.1.3 // @grafana/grafana-app-platform-squad
|
||||
github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90 // @grafana/grafana-backend-group
|
||||
@@ -401,7 +401,7 @@ require (
|
||||
github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/docker v28.4.0+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect; @grafana/grafana-app-platform-squad
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 // indirect
|
||||
github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect
|
||||
@@ -653,7 +653,15 @@ require (
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
require github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec // @grafana/observability-traces-and-profiling
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
|
||||
@@ -663,6 +671,20 @@ require (
|
||||
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
)
|
||||
|
||||
// Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream
|
||||
|
||||
46
go.sum
46
go.sum
@@ -645,6 +645,8 @@ gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGq
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||
github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2 h1:qFYgLH2zZe3WHpQgUrzeazC+ebDebwAQqS9yE1cP5Bs=
|
||||
github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2/go.mod h1:09/ALd1AXCTCOfcJYD8+jIYKmFmi6PVCkTsipC18F7E=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
|
||||
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
|
||||
github.com/Azure/azure-sdk-for-go v23.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
@@ -1051,6 +1053,8 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
@@ -1060,12 +1064,16 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
|
||||
github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg=
|
||||
github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc=
|
||||
@@ -1132,6 +1140,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
|
||||
@@ -1251,6 +1261,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
|
||||
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
|
||||
github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
|
||||
@@ -1942,6 +1954,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/linode/linodego v1.47.0 h1:6MFNCyzWbr8Rhl4r7d5DwZLwxvFIsM4ARH6W0KS/R0U=
|
||||
github.com/linode/linodego v1.47.0/go.mod h1:vyklQRzZUWhFVBZdYx4dcYJU/gG9yKB9VUcUs6ub0Lk=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
|
||||
@@ -1953,6 +1967,8 @@ github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
@@ -2066,12 +2082,20 @@ github.com/mithrandie/ternary v1.1.1 h1:k/joD6UGVYxHixYmSR8EGgDFNONBMqyD373xT4QR
|
||||
github.com/mithrandie/ternary v1.1.1/go.mod h1:0D9Ba3+09K2TdSZO7/bFCC0GjSXetCvYuYq0u8FY/1g=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
||||
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/mocktools/go-smtp-mock/v2 v2.5.1 h1:QcMJMChSgG1olVj4o6xxQFdrWzRjYNrcq660HAjd0wA=
|
||||
@@ -2218,6 +2242,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/pressly/goose/v3 v3.25.0 h1:6WeYhMWGRCzpyd89SpODFnCBCKz41KrVbRT58nVjGng=
|
||||
github.com/pressly/goose/v3 v3.25.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
@@ -2365,6 +2391,8 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3 h1:/4/IJi5iyTdh6mqOUaASW148HQpujYiHl0Wl78dSOSc=
|
||||
github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3/go.mod h1:aJIMhRsunltJR926EB2MUg8qHemFQDreSB33pyto2Ps=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
@@ -2461,6 +2489,8 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
|
||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/testcontainers/testcontainers-go v0.36.0 h1:YpffyLuHtdp5EUsI5mT4sRw8GZhO/5ozyDT1xWGXt00=
|
||||
github.com/testcontainers/testcontainers-go v0.36.0/go.mod h1:yk73GVJ0KUZIHUtFna6MO7QS144qYpoY8lEEtU9Hed0=
|
||||
github.com/thanos-io/objstore v0.0.0-20240818203309-0363dadfdfb1 h1:z0v9BB/p7s4J6R//+0a5M3wCld8KzNjrGRLIwXfrAZk=
|
||||
github.com/thanos-io/objstore v0.0.0-20240818203309-0363dadfdfb1/go.mod h1:3ukSkG4rIRUGkKM4oIz+BSuUx2e3RlQVVv3Cc3W+Tv4=
|
||||
github.com/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM=
|
||||
@@ -2471,6 +2501,10 @@ github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1C
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tjhop/slog-gokit v0.1.3 h1:6SdexP3UIeg93KLFeiM1Wp1caRwdTLgsD/THxBUy1+o=
|
||||
github.com/tjhop/slog-gokit v0.1.3/go.mod h1:Bbu5v2748qpAWH7k6gse/kw3076IJf6owJmh7yArmJs=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
|
||||
github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc=
|
||||
@@ -2547,6 +2581,8 @@ github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBU
|
||||
github.com/yuin/gopher-lua v0.0.0-20191213034115-f46add6fdb5c/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
|
||||
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
|
||||
@@ -2995,6 +3031,7 @@ golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -3027,6 +3064,7 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -3609,8 +3647,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
|
||||
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
338
go.work.sum
338
go.work.sum
File diff suppressed because it is too large
Load Diff
@@ -35,17 +35,20 @@ const sourceFiles = teamFiles.filter((file) => {
|
||||
const ext = path.extname(file);
|
||||
return (
|
||||
['.ts', '.tsx', '.js', '.jsx'].includes(ext) &&
|
||||
// exclude all tests
|
||||
// exclude all tests and mocks
|
||||
!path.matchesGlob(file, '**/test/**/*') &&
|
||||
!file.includes('.test.') &&
|
||||
!file.includes('.spec.') &&
|
||||
!path.matchesGlob(file, '**/__mocks__/**/*') &&
|
||||
// and storybook stories
|
||||
!file.includes('.story.') &&
|
||||
// and generated files
|
||||
!file.includes('.gen.ts') &&
|
||||
// and type definitions
|
||||
!file.includes('.d.ts') &&
|
||||
!file.endsWith('/types.ts')
|
||||
!file.endsWith('/types.ts') &&
|
||||
// and anything in graveyard
|
||||
!path.matchesGlob(file, '**/graveyard/**/*')
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -845,6 +845,7 @@ export type DashboardAccess = {
|
||||
/** The permissions part */
|
||||
canSave: boolean;
|
||||
canStar: boolean;
|
||||
isPublic: boolean;
|
||||
/** Metadata fields */
|
||||
slug?: string;
|
||||
url?: string;
|
||||
|
||||
@@ -729,6 +729,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
timeRangeProvider?: boolean;
|
||||
/**
|
||||
* Enables time range panning functionality
|
||||
*/
|
||||
timeRangePan?: boolean;
|
||||
/**
|
||||
* Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default.
|
||||
* @default false
|
||||
*/
|
||||
@@ -1246,9 +1250,4 @@ export interface FeatureToggles {
|
||||
* Enable template dashboards
|
||||
*/
|
||||
dashboardTemplates?: boolean;
|
||||
/**
|
||||
* Enables the ability to create multiple alerting policies
|
||||
* @default false
|
||||
*/
|
||||
alertingMultiplePolicies?: boolean;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ const dashboardToAppPlatform = (dashboard: (typeof mockTree)[number]['item']) =>
|
||||
},
|
||||
status: {},
|
||||
// TODO: Eventually add access properties, as required by tests
|
||||
access: {},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export const getCardContainerStyles = (
|
||||
display: 'grid',
|
||||
position: 'relative',
|
||||
gridTemplateColumns: 'auto 1fr auto',
|
||||
gridTemplateRows: '1fr auto auto auto',
|
||||
gridTemplateRows: 'auto auto 1fr auto',
|
||||
gridAutoColumns: '1fr',
|
||||
gridAutoFlow: 'row',
|
||||
gridTemplateAreas: `
|
||||
|
||||
@@ -212,7 +212,7 @@ func (sk8s *shortURLK8sHandler) createKubernetesShortURLsHandler(c *contextmodel
|
||||
|
||||
c.Logger.Debug("Creating short URL", "path", cmd.Path)
|
||||
obj := shorturl.LegacyCreateCommandToUnstructured(cmd)
|
||||
obj.SetGenerateName("u") // becomes a prefix
|
||||
obj.SetGenerateName("s") // becomes a prefix
|
||||
|
||||
out, err := client.Create(c.Req.Context(), &obj, v1.CreateOptions{})
|
||||
if err != nil {
|
||||
|
||||
@@ -5,7 +5,7 @@ go 1.25.3
|
||||
// Override docker/docker to avoid:
|
||||
// go: github.com/drone-runners/drone-runner-docker@v1.8.2 requires
|
||||
// github.com/docker/docker@v0.0.0-00010101000000-000000000000: invalid version: unknown revision 000000000000
|
||||
replace github.com/docker/docker => github.com/moby/moby v27.5.1+incompatible
|
||||
replace github.com/docker/docker => github.com/moby/moby v28.0.1+incompatible
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect; @grafana/grafana-backend-group
|
||||
|
||||
@@ -58,4 +58,5 @@ import (
|
||||
|
||||
_ "github.com/grafana/grafana/apps/alerting/alertenrichment/pkg/apis/alertenrichment/v1beta1"
|
||||
_ "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1"
|
||||
_ "github.com/testcontainers/testcontainers-go"
|
||||
)
|
||||
|
||||
@@ -3,11 +3,22 @@ package middleware
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
var (
|
||||
hostRedirectCounter = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "host_redirect_total",
|
||||
Help: "Number of requests redirected due to host header mismatch",
|
||||
Namespace: "grafana",
|
||||
})
|
||||
)
|
||||
|
||||
func ValidateHostHeader(cfg *setting.Cfg) web.Handler {
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
// ignore local render calls
|
||||
@@ -21,6 +32,8 @@ func ValidateHostHeader(cfg *setting.Cfg) web.Handler {
|
||||
}
|
||||
|
||||
if !strings.EqualFold(h, cfg.Domain) {
|
||||
hostRedirectCounter.Inc()
|
||||
c.Logger.Info("Enforcing Host header", "hosted", c.Req.Host, "expected", cfg.Domain)
|
||||
c.Redirect(strings.TrimSuffix(cfg.AppURL, "/")+c.Req.RequestURI, 301)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||
"github.com/grafana/grafana/pkg/services/librarypanels"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -112,6 +113,7 @@ type DashboardsAPIBuilder struct {
|
||||
dualWriter dualwrite.Service
|
||||
folderClientProvider client.K8sHandlerProvider
|
||||
libraryPanels libraryelements.Service // for legacy library panels
|
||||
publicDashboardService publicdashboards.Service
|
||||
|
||||
isStandalone bool // skips any handling including anything to do with legacy storage
|
||||
}
|
||||
@@ -140,6 +142,7 @@ func RegisterAPIService(
|
||||
restConfigProvider apiserver.RestConfigProvider,
|
||||
userService user.Service,
|
||||
libraryPanels libraryelements.Service,
|
||||
publicDashboardService publicdashboards.Service,
|
||||
) *DashboardsAPIBuilder {
|
||||
dbp := legacysql.NewDatabaseProvider(sql)
|
||||
namespacer := request.GetNamespaceMapper(cfg)
|
||||
@@ -163,6 +166,7 @@ func RegisterAPIService(
|
||||
dualWriter: dual,
|
||||
folderClientProvider: newSimpleFolderClientProvider(folderClient),
|
||||
libraryPanels: libraryPanels,
|
||||
publicDashboardService: publicDashboardService,
|
||||
|
||||
legacy: &DashboardStorage{
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, dashboardPermissionsSvc, accessControl, features),
|
||||
@@ -652,6 +656,7 @@ func (b *DashboardsAPIBuilder) storageForVersion(
|
||||
b.accessControl,
|
||||
opts.Scheme,
|
||||
newDTOFunc,
|
||||
b.publicDashboardService,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
@@ -28,13 +29,14 @@ type dtoBuilder = func(dashboard runtime.Object, access *dashboard.DashboardAcce
|
||||
|
||||
// The DTO returns everything the UI needs in a single request
|
||||
type DTOConnector struct {
|
||||
getter rest.Getter
|
||||
legacy legacy.DashboardAccess
|
||||
unified resource.ResourceClient
|
||||
largeObjects apistore.LargeObjectSupport
|
||||
accessControl accesscontrol.AccessControl
|
||||
scheme *runtime.Scheme
|
||||
builder dtoBuilder
|
||||
getter rest.Getter
|
||||
legacy legacy.DashboardAccess
|
||||
unified resource.ResourceClient
|
||||
largeObjects apistore.LargeObjectSupport
|
||||
accessControl accesscontrol.AccessControl
|
||||
scheme *runtime.Scheme
|
||||
builder dtoBuilder
|
||||
publicDashboardService publicdashboards.Service
|
||||
}
|
||||
|
||||
func NewDTOConnector(
|
||||
@@ -45,15 +47,17 @@ func NewDTOConnector(
|
||||
accessControl accesscontrol.AccessControl,
|
||||
scheme *runtime.Scheme,
|
||||
builder dtoBuilder,
|
||||
publicDashboardService publicdashboards.Service,
|
||||
) (rest.Storage, error) {
|
||||
return &DTOConnector{
|
||||
getter: getter,
|
||||
legacy: legacyAccess,
|
||||
accessControl: accessControl,
|
||||
unified: resourceClient,
|
||||
largeObjects: largeObjects,
|
||||
builder: builder,
|
||||
scheme: scheme,
|
||||
getter: getter,
|
||||
legacy: legacyAccess,
|
||||
accessControl: accessControl,
|
||||
unified: resourceClient,
|
||||
largeObjects: largeObjects,
|
||||
builder: builder,
|
||||
scheme: scheme,
|
||||
publicDashboardService: publicDashboardService,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -154,6 +158,11 @@ func (r *DTOConnector) Connect(ctx context.Context, name string, opts runtime.Ob
|
||||
access.Slug = slugify.Slugify(title)
|
||||
access.Url = dashboards.GetDashboardFolderURL(false, name, access.Slug)
|
||||
|
||||
pubDash, err := r.publicDashboardService.FindByDashboardUid(ctx, user.GetOrgID(), name)
|
||||
if err == nil && pubDash != nil {
|
||||
access.IsPublic = true
|
||||
}
|
||||
|
||||
dash, err := r.builder(rawobj, access)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
|
||||
@@ -146,7 +146,7 @@ func validateOnDelete(ctx context.Context,
|
||||
|
||||
for _, v := range resp.Stats {
|
||||
if v.Count > 0 {
|
||||
return folder.ErrFolderNotEmpty.Errorf("folder is not empty, contains %d resources", v.Count)
|
||||
return folder.ErrFolderNotEmpty.Errorf("folder is not empty, contains %d %s.%s", v.Count, v.Group, v.Resource)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -86,6 +86,7 @@ func (b *IdentityAccessManagementAPIBuilder) AfterTeamBindingCreate(obj runtime.
|
||||
"name", tb.Name,
|
||||
"subject", tb.Spec.Subject.Name,
|
||||
"teamRef", tb.Spec.TeamRef.Name,
|
||||
"permission", tb.Spec.Permission,
|
||||
"err", err,
|
||||
)
|
||||
status = "failure"
|
||||
@@ -117,6 +118,7 @@ func (b *IdentityAccessManagementAPIBuilder) AfterTeamBindingCreate(obj runtime.
|
||||
"name", tb.Name,
|
||||
"subject", tb.Spec.Subject.Name,
|
||||
"teamRef", tb.Spec.TeamRef.Name,
|
||||
"permission", tb.Spec.Permission,
|
||||
)
|
||||
} else {
|
||||
// Record successful tuple write
|
||||
@@ -143,6 +145,20 @@ func (b *IdentityAccessManagementAPIBuilder) BeginTeamBindingUpdate(ctx context.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if oldTB.Spec.Subject.Name == newTB.Spec.Subject.Name && oldTB.Spec.TeamRef.Name == newTB.Spec.TeamRef.Name && oldTB.Spec.Permission == newTB.Spec.Permission {
|
||||
return nil, nil // No changes to the team binding
|
||||
}
|
||||
|
||||
if newTB.Spec.Subject.Name == "" || newTB.Spec.TeamRef.Name == "" {
|
||||
b.logger.Error("invalid team binding",
|
||||
"namespace", newTB.Namespace,
|
||||
"name", newTB.Name,
|
||||
"subject", newTB.Spec.Subject.Name,
|
||||
"teamRef", newTB.Spec.TeamRef.Name,
|
||||
)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Convert old team binding to tuple for deletion
|
||||
var oldTuple *v1.TupleKey
|
||||
var oldErr error
|
||||
@@ -154,6 +170,7 @@ func (b *IdentityAccessManagementAPIBuilder) BeginTeamBindingUpdate(ctx context.
|
||||
"name", oldTB.Name,
|
||||
"err", oldErr,
|
||||
)
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,18 +185,16 @@ func (b *IdentityAccessManagementAPIBuilder) BeginTeamBindingUpdate(ctx context.
|
||||
"name", newTB.Name,
|
||||
"err", newErr,
|
||||
)
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Return a finish function that performs the zanzana write only on success
|
||||
return func(ctx context.Context, success bool) {
|
||||
if !success {
|
||||
// Update failed, don't write to zanzana
|
||||
return
|
||||
}
|
||||
|
||||
// Grab a ticket to write to Zanzana
|
||||
// This limits the amount of concurrent connections to Zanzana
|
||||
wait := time.Now()
|
||||
b.zTickets <- true
|
||||
hooksWaitHistogram.WithLabelValues("teambinding", "update").Observe(time.Since(wait).Seconds())
|
||||
|
||||
@@ -487,6 +487,223 @@ func TestBeginTeamBindingUpdate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, finishFunc) // Should return nil when zClient is nil
|
||||
})
|
||||
|
||||
t.Run("should handle empty old binding subject name gracefully", func(t *testing.T) {
|
||||
wg.Add(1)
|
||||
oldBinding := iamv0.TeamBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "binding-6",
|
||||
Namespace: "org-6",
|
||||
},
|
||||
Spec: iamv0.TeamBindingSpec{
|
||||
Subject: iamv0.TeamBindingspecSubject{
|
||||
Name: "", // Empty name - conversion will be skipped
|
||||
},
|
||||
TeamRef: iamv0.TeamBindingTeamRef{
|
||||
Name: "team-1",
|
||||
},
|
||||
Permission: iamv0.TeamBindingTeamPermissionMember,
|
||||
},
|
||||
}
|
||||
|
||||
newBinding := iamv0.TeamBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "binding-6",
|
||||
Namespace: "org-6",
|
||||
},
|
||||
Spec: iamv0.TeamBindingSpec{
|
||||
Subject: iamv0.TeamBindingspecSubject{
|
||||
Name: "user-2",
|
||||
},
|
||||
TeamRef: iamv0.TeamBindingTeamRef{
|
||||
Name: "team-1",
|
||||
},
|
||||
Permission: iamv0.TeamBindingTeamPermissionMember,
|
||||
},
|
||||
}
|
||||
|
||||
testEmptyOldBinding := func(ctx context.Context, req *v1.WriteRequest) error {
|
||||
defer wg.Done()
|
||||
require.NotNil(t, req)
|
||||
require.Equal(t, "org-6", req.Namespace)
|
||||
|
||||
// Should not delete old binding (it was skipped due to empty name)
|
||||
require.Nil(t, req.Deletes)
|
||||
|
||||
// Should write new binding
|
||||
require.NotNil(t, req.Writes)
|
||||
require.Len(t, req.Writes.TupleKeys, 1)
|
||||
require.Equal(
|
||||
t,
|
||||
req.Writes.TupleKeys[0],
|
||||
&v1.TupleKey{User: "user:user-2", Relation: "member", Object: "team:team-1"},
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
b.zClient = &FakeZanzanaClient{writeCallback: testEmptyOldBinding}
|
||||
|
||||
finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, finishFunc) // Should still return finish function
|
||||
|
||||
finishFunc(context.Background(), true)
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("should return nil finish func when bindings are identical", func(t *testing.T) {
|
||||
oldBinding := iamv0.TeamBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "binding-7",
|
||||
Namespace: "org-7",
|
||||
},
|
||||
Spec: iamv0.TeamBindingSpec{
|
||||
Subject: iamv0.TeamBindingspecSubject{
|
||||
Name: "user-1",
|
||||
},
|
||||
TeamRef: iamv0.TeamBindingTeamRef{
|
||||
Name: "team-1",
|
||||
},
|
||||
Permission: iamv0.TeamBindingTeamPermissionMember,
|
||||
},
|
||||
}
|
||||
|
||||
newBinding := iamv0.TeamBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "binding-7",
|
||||
Namespace: "org-7",
|
||||
},
|
||||
Spec: iamv0.TeamBindingSpec{
|
||||
Subject: iamv0.TeamBindingspecSubject{
|
||||
Name: "user-1",
|
||||
},
|
||||
TeamRef: iamv0.TeamBindingTeamRef{
|
||||
Name: "team-1",
|
||||
},
|
||||
Permission: iamv0.TeamBindingTeamPermissionMember,
|
||||
},
|
||||
}
|
||||
|
||||
writeCalled := false
|
||||
testNoWriteOnNoChange := func(ctx context.Context, req *v1.WriteRequest) error {
|
||||
writeCalled = true
|
||||
require.Fail(t, "Write should not be called when bindings are identical")
|
||||
return nil
|
||||
}
|
||||
|
||||
b.zClient = &FakeZanzanaClient{writeCallback: testNoWriteOnNoChange}
|
||||
|
||||
finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, finishFunc) // Should return nil when bindings are identical
|
||||
|
||||
// Verify write was never called
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
require.False(t, writeCalled, "Write callback should not be called when bindings are identical")
|
||||
})
|
||||
|
||||
t.Run("should return nil finish func when new binding has empty subject name", func(t *testing.T) {
|
||||
oldBinding := iamv0.TeamBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "binding-8",
|
||||
Namespace: "org-8",
|
||||
},
|
||||
Spec: iamv0.TeamBindingSpec{
|
||||
Subject: iamv0.TeamBindingspecSubject{
|
||||
Name: "user-1",
|
||||
},
|
||||
TeamRef: iamv0.TeamBindingTeamRef{
|
||||
Name: "team-1",
|
||||
},
|
||||
Permission: iamv0.TeamBindingTeamPermissionMember,
|
||||
},
|
||||
}
|
||||
|
||||
newBinding := iamv0.TeamBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "binding-8",
|
||||
Namespace: "org-8",
|
||||
},
|
||||
Spec: iamv0.TeamBindingSpec{
|
||||
Subject: iamv0.TeamBindingspecSubject{
|
||||
Name: "", // Empty name - should cause early return
|
||||
},
|
||||
TeamRef: iamv0.TeamBindingTeamRef{
|
||||
Name: "team-1",
|
||||
},
|
||||
Permission: iamv0.TeamBindingTeamPermissionMember,
|
||||
},
|
||||
}
|
||||
|
||||
writeCalled := false
|
||||
testNoWriteOnInvalidBinding := func(ctx context.Context, req *v1.WriteRequest) error {
|
||||
writeCalled = true
|
||||
require.Fail(t, "Write should not be called when new binding has empty subject name")
|
||||
return nil
|
||||
}
|
||||
|
||||
b.zClient = &FakeZanzanaClient{writeCallback: testNoWriteOnInvalidBinding}
|
||||
|
||||
finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, finishFunc) // Should return nil when new binding has empty subject name
|
||||
|
||||
// Verify write was never called
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
require.False(t, writeCalled, "Write callback should not be called when new binding has empty subject name")
|
||||
})
|
||||
|
||||
t.Run("should return nil finish func when new binding has empty team ref name", func(t *testing.T) {
|
||||
oldBinding := iamv0.TeamBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "binding-9",
|
||||
Namespace: "org-9",
|
||||
},
|
||||
Spec: iamv0.TeamBindingSpec{
|
||||
Subject: iamv0.TeamBindingspecSubject{
|
||||
Name: "user-1",
|
||||
},
|
||||
TeamRef: iamv0.TeamBindingTeamRef{
|
||||
Name: "team-1",
|
||||
},
|
||||
Permission: iamv0.TeamBindingTeamPermissionMember,
|
||||
},
|
||||
}
|
||||
|
||||
newBinding := iamv0.TeamBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "binding-9",
|
||||
Namespace: "org-9",
|
||||
},
|
||||
Spec: iamv0.TeamBindingSpec{
|
||||
Subject: iamv0.TeamBindingspecSubject{
|
||||
Name: "user-2",
|
||||
},
|
||||
TeamRef: iamv0.TeamBindingTeamRef{
|
||||
Name: "", // Empty name - should cause early return
|
||||
},
|
||||
Permission: iamv0.TeamBindingTeamPermissionMember,
|
||||
},
|
||||
}
|
||||
|
||||
writeCalled := false
|
||||
testNoWriteOnInvalidBinding := func(ctx context.Context, req *v1.WriteRequest) error {
|
||||
writeCalled = true
|
||||
require.Fail(t, "Write should not be called when new binding has empty team ref name")
|
||||
return nil
|
||||
}
|
||||
|
||||
b.zClient = &FakeZanzanaClient{writeCallback: testNoWriteOnInvalidBinding}
|
||||
|
||||
finishFunc, err := b.BeginTeamBindingUpdate(context.Background(), &newBinding, &oldBinding, nil)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, finishFunc) // Should return nil when new binding has empty team ref name
|
||||
|
||||
// Verify write was never called
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
require.False(t, writeCalled, "Write callback should not be called when new binding has empty team ref name")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAfterTeamBindingDelete(t *testing.T) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -74,6 +75,11 @@ type jobDriver struct {
|
||||
|
||||
// notifications channel for job create events
|
||||
notifications chan struct{}
|
||||
|
||||
// Mutex to protect concurrent access to job processing
|
||||
mu sync.Mutex
|
||||
// currentJob is the job currently being processed
|
||||
currentJob *provisioning.Job
|
||||
}
|
||||
|
||||
func NewJobDriver(
|
||||
@@ -142,7 +148,7 @@ func (d *jobDriver) claimAndProcessOneJob(ctx context.Context) error {
|
||||
logger := logging.FromContext(ctx)
|
||||
|
||||
// Claim a job to work on.
|
||||
job, rollback, err := d.store.Claim(ctx)
|
||||
claimedJob, rollback, err := d.store.Claim(ctx)
|
||||
if err != nil {
|
||||
return apifmt.Errorf("failed to claim job: %w", err)
|
||||
}
|
||||
@@ -150,14 +156,16 @@ func (d *jobDriver) claimAndProcessOneJob(ctx context.Context) error {
|
||||
// The rollback function does not care about cancellations.
|
||||
defer rollback()
|
||||
|
||||
logger = logger.With("job", job.GetName(), "namespace", job.GetNamespace())
|
||||
namespace := claimedJob.GetNamespace()
|
||||
logger = logger.With("job", claimedJob.GetName(), "namespace", namespace)
|
||||
ctx = logging.Context(ctx, logger)
|
||||
logger.Debug("claimed a job")
|
||||
d.currentJob = claimedJob
|
||||
|
||||
// Now that we have a job, we need to augment our namespace to grant ourselves permission to work on it.
|
||||
// Incidentally, this also limits our permissions to only the namespace of the job.
|
||||
ctx = request.WithNamespace(ctx, job.GetNamespace())
|
||||
ctx, _, err = identity.WithProvisioningIdentity(ctx, job.GetNamespace())
|
||||
ctx = request.WithNamespace(ctx, namespace)
|
||||
ctx, _, err = identity.WithProvisioningIdentity(ctx, namespace)
|
||||
if err != nil {
|
||||
return apifmt.Errorf("failed to grant provisioning identity: %w", err)
|
||||
}
|
||||
@@ -169,37 +177,42 @@ func (d *jobDriver) claimAndProcessOneJob(ctx context.Context) error {
|
||||
leaseRenewalCtx, cancelLeaseRenewal := context.WithCancel(jobctx)
|
||||
leaseExpired := make(chan struct{})
|
||||
|
||||
go d.leaseRenewalLoop(leaseRenewalCtx, job, logger, leaseExpired)
|
||||
go d.leaseRenewalLoop(leaseRenewalCtx, logger, leaseExpired)
|
||||
defer cancelLeaseRenewal()
|
||||
|
||||
recorder := newJobProgressRecorder(d.onProgress(job))
|
||||
recorder := newJobProgressRecorder(d.onProgress())
|
||||
recorder.SetMessage(ctx, "start job")
|
||||
|
||||
// Process the job with lease loss detection
|
||||
start := time.Now()
|
||||
job.Status.Started = start.UnixMilli()
|
||||
err = d.processJobWithLeaseCheck(jobctx, job, recorder, leaseExpired)
|
||||
err = d.processJobWithLeaseCheck(jobctx, recorder, leaseExpired)
|
||||
end := time.Now()
|
||||
logger.Debug("job processed", "duration", end.Sub(start), "error", err)
|
||||
logger.Debug("job processed", "duration", end.Sub(recorder.Started()), "error", err)
|
||||
|
||||
// Capture job timeout
|
||||
if jobctx.Err() != nil && err == nil {
|
||||
err = jobctx.Err()
|
||||
}
|
||||
|
||||
job.Status = recorder.Complete(ctx, err)
|
||||
// Complete the job
|
||||
d.mu.Lock()
|
||||
d.currentJob.Status = recorder.Complete(ctx, err)
|
||||
defer func() {
|
||||
d.currentJob = nil
|
||||
d.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Save the finished job
|
||||
err = d.historicJobs.WriteJob(ctx, job.DeepCopy())
|
||||
err = d.historicJobs.WriteJob(ctx, d.currentJob.DeepCopy())
|
||||
if err != nil {
|
||||
// We're not going to return this as it is not critical. Not ideal, but not critical.
|
||||
logger.Warn("failed to create historic job", "historic_job", *job, "error", err)
|
||||
logger.Warn("failed to create historic job", "historic_job", *d.currentJob, "error", err)
|
||||
} else {
|
||||
logger.Debug("created historic job", "historic_job", *job)
|
||||
logger.Debug("created historic job", "historic_job", *d.currentJob)
|
||||
}
|
||||
|
||||
// Mark the job as completed.
|
||||
if err := d.store.Complete(ctx, job); err != nil {
|
||||
return apifmt.Errorf("failed to complete job '%s' in '%s': %w", job.GetName(), job.GetNamespace(), err)
|
||||
if err := d.store.Complete(ctx, d.currentJob); err != nil {
|
||||
return apifmt.Errorf("failed to complete job '%s' in '%s': %w", d.currentJob.GetName(), d.currentJob.GetNamespace(), err)
|
||||
}
|
||||
logger.Debug("job completed")
|
||||
|
||||
@@ -208,7 +221,7 @@ func (d *jobDriver) claimAndProcessOneJob(ctx context.Context) error {
|
||||
|
||||
// leaseRenewalLoop continuously renews the lease for a job until the context is cancelled.
|
||||
// If lease renewal fails persistently, it signals via the leaseExpired channel.
|
||||
func (d *jobDriver) leaseRenewalLoop(ctx context.Context, job *provisioning.Job, logger logging.Logger, leaseExpired chan struct{}) {
|
||||
func (d *jobDriver) leaseRenewalLoop(ctx context.Context, logger logging.Logger, leaseExpired chan struct{}) {
|
||||
ticker := time.NewTicker(d.leaseRenewalInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -223,7 +236,15 @@ func (d *jobDriver) leaseRenewalLoop(ctx context.Context, job *provisioning.Job,
|
||||
logger.Debug("lease renewal loop stopping")
|
||||
return
|
||||
case <-ticker.C:
|
||||
err := d.store.RenewLease(ctx, job)
|
||||
d.mu.Lock()
|
||||
if d.currentJob == nil {
|
||||
d.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
err := d.store.RenewLease(ctx, d.currentJob)
|
||||
d.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
consecutiveFailures++
|
||||
if apierrors.IsNotFound(err) ||
|
||||
@@ -253,11 +274,11 @@ func (d *jobDriver) leaseRenewalLoop(ctx context.Context, job *provisioning.Job,
|
||||
}
|
||||
|
||||
// processJobWithLeaseCheck processes a job but aborts if the lease expires.
|
||||
func (d *jobDriver) processJobWithLeaseCheck(ctx context.Context, job *provisioning.Job, recorder JobProgressRecorder, leaseExpired <-chan struct{}) error {
|
||||
func (d *jobDriver) processJobWithLeaseCheck(ctx context.Context, recorder JobProgressRecorder, leaseExpired <-chan struct{}) error {
|
||||
// Run the job processing in a goroutine so we can monitor lease expiry
|
||||
resultChan := make(chan error, 1)
|
||||
go func() {
|
||||
resultChan <- d.processJob(ctx, job, recorder)
|
||||
resultChan <- d.processJob(ctx, recorder)
|
||||
}()
|
||||
|
||||
select {
|
||||
@@ -270,16 +291,28 @@ func (d *jobDriver) processJobWithLeaseCheck(ctx context.Context, job *provision
|
||||
}
|
||||
}
|
||||
|
||||
func (d *jobDriver) processJob(ctx context.Context, job *provisioning.Job, recorder JobProgressRecorder) error {
|
||||
func (d *jobDriver) processJob(ctx context.Context, recorder JobProgressRecorder) error {
|
||||
logger := logging.FromContext(ctx)
|
||||
d.mu.Lock()
|
||||
if d.currentJob == nil {
|
||||
d.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Here it's safe to copy as only job spec is used for processing
|
||||
job := d.currentJob.DeepCopy()
|
||||
repoName := d.currentJob.Spec.Repository
|
||||
namespace := d.currentJob.Namespace
|
||||
d.mu.Unlock()
|
||||
|
||||
for _, worker := range d.workers {
|
||||
if !worker.IsSupported(ctx, *job) {
|
||||
continue
|
||||
}
|
||||
|
||||
repo, err := d.repoGetter.GetRepository(ctx, job.Namespace, job.Spec.Repository)
|
||||
repo, err := d.repoGetter.GetRepository(ctx, namespace, repoName)
|
||||
if err != nil {
|
||||
return apifmt.Errorf("failed to get repository '%s': %w", job.Spec.Repository, err)
|
||||
return apifmt.Errorf("failed to get repository '%s': %w", repoName, err)
|
||||
}
|
||||
|
||||
r := repo.Config()
|
||||
@@ -298,42 +331,51 @@ func (d *jobDriver) processJob(ctx context.Context, job *provisioning.Job, recor
|
||||
return apifmt.Errorf("no workers were registered to handle the job")
|
||||
}
|
||||
|
||||
func (d *jobDriver) onProgress(job *provisioning.Job) ProgressFn {
|
||||
func (d *jobDriver) onProgress() ProgressFn {
|
||||
return func(ctx context.Context, status provisioning.JobStatus) error {
|
||||
logging.FromContext(ctx).Debug("job progress", "status", status)
|
||||
|
||||
const maxRetries = 3
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
// Use the current job for the first attempt, fetch fresh for retries
|
||||
currentJob := job
|
||||
d.mu.Lock()
|
||||
if d.currentJob == nil {
|
||||
d.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use the current job for the first attempt; on retry attempts, fetch fresh data from the store to resolve conflicts
|
||||
if attempt > 0 {
|
||||
// Fetch the latest version to resolve conflicts
|
||||
latest, err := d.store.Get(ctx, job.GetNamespace(), job.GetName())
|
||||
latest, err := d.store.Get(ctx, d.currentJob.GetNamespace(), d.currentJob.GetName())
|
||||
if err != nil {
|
||||
d.mu.Unlock()
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Job was completed/deleted, nothing to update
|
||||
return nil
|
||||
}
|
||||
return apifmt.Errorf("failed to fetch job for progress update: %w", err)
|
||||
}
|
||||
currentJob = latest
|
||||
|
||||
*d.currentJob = *latest
|
||||
}
|
||||
|
||||
job := d.currentJob
|
||||
// Update status on the current job
|
||||
currentJob.Status = status
|
||||
|
||||
updated, err := d.store.Update(ctx, currentJob)
|
||||
job.Status = status
|
||||
updated, err := d.store.Update(ctx, job)
|
||||
if err != nil {
|
||||
if apierrors.IsConflict(err) && attempt < maxRetries-1 {
|
||||
// Conflict detected, retry with fresh data
|
||||
logging.FromContext(ctx).Debug("progress update conflict, retrying", "attempt", attempt+1)
|
||||
continue
|
||||
}
|
||||
d.mu.Unlock()
|
||||
return apifmt.Errorf("failed to update job progress: %w", err)
|
||||
}
|
||||
|
||||
// Update succeeded, update our local copy
|
||||
*job = *updated
|
||||
*d.currentJob = *updated
|
||||
d.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package jobs
|
||||
|
||||
import (
|
||||
context "context"
|
||||
time "time"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
v0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockJobProgressRecorder is an autogenerated mock type for the JobProgressRecorder type
|
||||
@@ -271,6 +273,51 @@ func (_c *MockJobProgressRecorder_SetTotal_Call) RunAndReturn(run func(context.C
|
||||
return _c
|
||||
}
|
||||
|
||||
// Started provides a mock function with no fields
|
||||
func (_m *MockJobProgressRecorder) Started() time.Time {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Started")
|
||||
}
|
||||
|
||||
var r0 time.Time
|
||||
if rf, ok := ret.Get(0).(func() time.Time); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(time.Time)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockJobProgressRecorder_Started_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Started'
|
||||
type MockJobProgressRecorder_Started_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Started is a helper method to define mock.On call
|
||||
func (_e *MockJobProgressRecorder_Expecter) Started() *MockJobProgressRecorder_Started_Call {
|
||||
return &MockJobProgressRecorder_Started_Call{Call: _e.mock.On("Started")}
|
||||
}
|
||||
|
||||
func (_c *MockJobProgressRecorder_Started_Call) Run(run func()) *MockJobProgressRecorder_Started_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockJobProgressRecorder_Started_Call) Return(_a0 time.Time) *MockJobProgressRecorder_Started_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockJobProgressRecorder_Started_Call) RunAndReturn(run func() time.Time) *MockJobProgressRecorder_Started_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// StrictMaxErrors provides a mock function with given fields: maxErrors
|
||||
func (_m *MockJobProgressRecorder) StrictMaxErrors(maxErrors int) {
|
||||
_m.Called(maxErrors)
|
||||
|
||||
@@ -69,6 +69,10 @@ func newJobProgressRecorder(ProgressFn ProgressFn) JobProgressRecorder {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *jobProgressRecorder) Started() time.Time {
|
||||
return r.started
|
||||
}
|
||||
|
||||
func (r *jobProgressRecorder) Record(ctx context.Context, result JobResourceResult) {
|
||||
var shouldLogError bool
|
||||
var logErr error
|
||||
|
||||
@@ -2,6 +2,7 @@ package jobs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/provisioning/pkg/repository"
|
||||
@@ -18,6 +19,7 @@ type RepoGetter interface {
|
||||
//
|
||||
//go:generate mockery --name JobProgressRecorder --structname MockJobProgressRecorder --inpackage --filename job_progress_recorder_mock.go --with-expecter
|
||||
type JobProgressRecorder interface {
|
||||
Started() time.Time
|
||||
Record(ctx context.Context, result JobResourceResult)
|
||||
ResetResults()
|
||||
SetFinalMessage(ctx context.Context, msg string)
|
||||
|
||||
@@ -40,8 +40,6 @@ var expectedHeaders = map[string]string{
|
||||
strings.ToLower(queryService.HeaderPanelPluginId): queryService.HeaderPanelPluginId,
|
||||
strings.ToLower(queryService.HeaderDashboardTitle): queryService.HeaderDashboardTitle,
|
||||
strings.ToLower(queryService.HeaderPanelTitle): queryService.HeaderPanelTitle,
|
||||
strings.ToLower("X-Real-IP"): "X-Real-IP",
|
||||
strings.ToLower("X-Forwarded-For"): "X-Forwarded-For",
|
||||
}
|
||||
|
||||
func ExtractKnownHeaders(header http.Header) map[string]string {
|
||||
|
||||
@@ -16,28 +16,13 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func ConvertToK8sResources(orgID int64, routes legacy_storage.ManagedRoutes, namespacer request.NamespaceMapper) (*model.RoutingTreeList, error) {
|
||||
result := &model.RoutingTreeList{
|
||||
Items: make([]model.RoutingTree, 0, len(routes)),
|
||||
}
|
||||
for _, r := range routes {
|
||||
k8sResource, err := ConvertToK8sResource(orgID, r, namespacer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert route %q to k8s resource: %w", r.Name, err)
|
||||
}
|
||||
result.Items = append(result.Items, *k8sResource)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ConvertToK8sResource(orgID int64, r *legacy_storage.ManagedRoute, namespacer request.NamespaceMapper) (*model.RoutingTree, error) {
|
||||
func ConvertToK8sResource(orgID int64, r definitions.Route, version string, namespacer request.NamespaceMapper) (*model.RoutingTree, error) {
|
||||
spec := model.RoutingTreeSpec{
|
||||
Defaults: model.RoutingTreeRouteDefaults{
|
||||
GroupBy: r.GroupBy,
|
||||
GroupBy: r.GroupByStr,
|
||||
GroupWait: optionalPrometheusDurationToString(r.GroupWait),
|
||||
GroupInterval: optionalPrometheusDurationToString(r.GroupInterval),
|
||||
RepeatInterval: optionalPrometheusDurationToString(r.RepeatInterval),
|
||||
@@ -54,9 +39,9 @@ func ConvertToK8sResource(orgID int64, r *legacy_storage.ManagedRoute, namespace
|
||||
|
||||
var result = &model.RoutingTree{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: r.Name,
|
||||
Name: model.UserDefinedRoutingTreeName,
|
||||
Namespace: namespacer(orgID),
|
||||
ResourceVersion: r.Version,
|
||||
ResourceVersion: version,
|
||||
},
|
||||
Spec: spec,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,11 +22,9 @@ var (
|
||||
)
|
||||
|
||||
type RouteService interface {
|
||||
GetManagedRoutes(ctx context.Context, orgID int64) (legacy_storage.ManagedRoutes, error)
|
||||
GetManagedRoute(ctx context.Context, orgID int64, name string) (legacy_storage.ManagedRoute, error)
|
||||
DeleteManagedRoute(ctx context.Context, orgID int64, name string, p alerting_models.Provenance, version string) error
|
||||
CreateManagedRoute(ctx context.Context, orgID int64, name string, subtree definitions.Route, p alerting_models.Provenance) (*legacy_storage.ManagedRoute, error)
|
||||
UpdateManagedRoute(ctx context.Context, orgID int64, name string, subtree definitions.Route, p alerting_models.Provenance, version string) (*legacy_storage.ManagedRoute, error)
|
||||
GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, string, error)
|
||||
UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p alerting_models.Provenance, version string) (definitions.Route, string, error)
|
||||
ResetPolicyTree(ctx context.Context, orgID int64, p alerting_models.Provenance) (definitions.Route, error)
|
||||
}
|
||||
|
||||
type legacyStorage struct {
|
||||
@@ -58,76 +55,56 @@ func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Objec
|
||||
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) List(ctx context.Context, _ *internalversion.ListOptions) (runtime.Object, error) {
|
||||
func (s *legacyStorage) getUserDefinedRoutingTree(ctx context.Context) (*model.RoutingTree, error) {
|
||||
orgId, err := request.OrgIDForList(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
managedRoutes, err := s.service.GetManagedRoutes(ctx, orgId)
|
||||
res, version, err := s.service.GetPolicyTree(ctx, orgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ConvertToK8sResources(orgId, managedRoutes, s.namespacer)
|
||||
return ConvertToK8sResource(orgId, res, version, s.namespacer)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) List(ctx context.Context, _ *internalversion.ListOptions) (runtime.Object, error) {
|
||||
user, err := s.getUserDefinedRoutingTree(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.RoutingTreeList{
|
||||
Items: []model.RoutingTree{
|
||||
*user,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Get(ctx context.Context, name string, _ *metav1.GetOptions) (runtime.Object, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if name != model.UserDefinedRoutingTreeName {
|
||||
return nil, errors.NewNotFound(ResourceInfo.GroupResource(), name)
|
||||
}
|
||||
managedRoute, err := s.service.GetManagedRoute(ctx, info.OrgID, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ConvertToK8sResource(info.OrgID, &managedRoute, s.namespacer)
|
||||
return s.getUserDefinedRoutingTree(ctx)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Create(ctx context.Context,
|
||||
obj runtime.Object,
|
||||
createValidation rest.ValidateObjectFunc,
|
||||
func (s *legacyStorage) Create(_ context.Context,
|
||||
_ runtime.Object,
|
||||
_ rest.ValidateObjectFunc,
|
||||
_ *metav1.CreateOptions,
|
||||
) (runtime.Object, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if createValidation != nil {
|
||||
if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
p, ok := obj.(*model.RoutingTree)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected %s but got %s", ResourceInfo.GroupVersionKind(), obj.GetObjectKind().GroupVersionKind())
|
||||
}
|
||||
domainModel, _, err := convertToDomainModel(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
created, err := s.service.CreateManagedRoute(ctx, info.OrgID, p.Name, domainModel, alerting_models.ProvenanceNone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ConvertToK8sResource(info.OrgID, created, s.namespacer)
|
||||
return nil, errors.NewMethodNotSupported(ResourceInfo.GroupResource(), "create")
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Update(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
objInfo rest.UpdatedObjectInfo,
|
||||
_ rest.ValidateObjectFunc,
|
||||
updateValidation rest.ValidateObjectUpdateFunc,
|
||||
_ bool,
|
||||
_ *metav1.UpdateOptions,
|
||||
) (runtime.Object, bool, error) {
|
||||
func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, _ rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, _ bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||
if name != model.UserDefinedRoutingTreeName {
|
||||
return nil, false, errors.NewNotFound(ResourceInfo.GroupResource(), name)
|
||||
}
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
old, err := s.Get(ctx, name, nil)
|
||||
old, err := s.Get(ctx, model.UserDefinedRoutingTreeName, nil)
|
||||
if err != nil {
|
||||
return old, false, err
|
||||
}
|
||||
@@ -145,26 +122,24 @@ func (s *legacyStorage) Update(
|
||||
return nil, false, fmt.Errorf("expected %s but got %s", ResourceInfo.GroupVersionKind(), obj.GetObjectKind().GroupVersionKind())
|
||||
}
|
||||
|
||||
domainModel, version, err := convertToDomainModel(p)
|
||||
model, version, err := convertToDomainModel(p)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
updated, err := s.service.UpdateManagedRoute(ctx, info.OrgID, p.Name, domainModel, alerting_models.ProvenanceNone, version)
|
||||
updated, updatedVersion, err := s.service.UpdatePolicyTree(ctx, info.OrgID, model, alerting_models.ProvenanceNone, version)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
obj, err = ConvertToK8sResource(info.OrgID, updated, s.namespacer)
|
||||
obj, err = ConvertToK8sResource(info.OrgID, updated, updatedVersion, s.namespacer)
|
||||
return obj, false, err
|
||||
}
|
||||
|
||||
// Delete implements rest.GracefulDeleter. It is needed for API server to not crash when it registers DeleteCollection method
|
||||
func (s *legacyStorage) Delete(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
deleteValidation rest.ValidateObjectFunc,
|
||||
options *metav1.DeleteOptions,
|
||||
) (runtime.Object, bool, error) {
|
||||
func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, opts *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
if name != model.UserDefinedRoutingTreeName {
|
||||
return nil, false, errors.NewNotFound(ResourceInfo.GroupResource(), name)
|
||||
}
|
||||
info, err := request.NamespaceInfoFrom(ctx, true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
@@ -180,14 +155,10 @@ func (s *legacyStorage) Delete(
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
version := ""
|
||||
if options.Preconditions != nil && options.Preconditions.ResourceVersion != nil {
|
||||
version = *options.Preconditions.ResourceVersion
|
||||
}
|
||||
err = s.service.DeleteManagedRoute(ctx, info.OrgID, name, alerting_models.ProvenanceNone, version) // TODO add support for dry-run option
|
||||
_, err = s.service.ResetPolicyTree(ctx, info.OrgID, alerting_models.ProvenanceNone) // TODO add support for dry-run option
|
||||
return old, false, err
|
||||
}
|
||||
|
||||
func (s *legacyStorage) DeleteCollection(_ context.Context, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions, _ *internalversion.ListOptions) (runtime.Object, error) {
|
||||
return nil, errors.NewMethodNotSupported(ResourceInfo.GroupResource(), "deleteCollection")
|
||||
return nil, errors.NewMethodNotSupported(ResourceInfo.GroupResource(), "delete")
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package routingtree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1"
|
||||
@@ -16,18 +14,5 @@ var ResourceInfo = utils.NewResourceInfo(kind.Group(), kind.Version(),
|
||||
kind.GroupVersionResource().Resource, strings.ToLower(kind.Kind()), kind.Kind(),
|
||||
func() runtime.Object { return kind.ZeroValue() },
|
||||
func() runtime.Object { return kind.ZeroListValue() },
|
||||
utils.TableColumns{
|
||||
Definition: []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
},
|
||||
Reader: func(obj any) ([]interface{}, error) {
|
||||
r, ok := obj.(*model.RoutingTree)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected resource or info")
|
||||
}
|
||||
return []interface{}{
|
||||
r.Name,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
utils.TableColumns{},
|
||||
)
|
||||
|
||||
4
pkg/server/wire_gen.go
generated
4
pkg/server/wire_gen.go
generated
@@ -840,7 +840,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService)
|
||||
ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService)
|
||||
apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService)
|
||||
dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService)
|
||||
dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl)
|
||||
snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer)
|
||||
dataSourceAPIBuilder, err := datasource.RegisterAPIService(configProvider, featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer)
|
||||
if err != nil {
|
||||
@@ -1474,7 +1474,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService)
|
||||
ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService)
|
||||
apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService)
|
||||
dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService)
|
||||
dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl)
|
||||
snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer)
|
||||
dataSourceAPIBuilder, err := datasource.RegisterAPIService(configProvider, featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer)
|
||||
if err != nil {
|
||||
|
||||
@@ -203,6 +203,14 @@ func createPostStartHook(
|
||||
logger.Error("Failed to initialize app", "app", installer.ManifestData().AppName, "error", err)
|
||||
return fmt.Errorf("failed to get app from installer %s: %w", installer.ManifestData().AppName, err)
|
||||
}
|
||||
return app.Runner().Run(hookContext.Context)
|
||||
go func() {
|
||||
err := app.Runner().Run(hookContext.Context)
|
||||
if err != nil {
|
||||
logger.Error("App runner exited with error", "app", installer.ManifestData().AppName, "error", err)
|
||||
} else {
|
||||
logger.Info("App runner exited without error", "app", installer.ManifestData().AppName)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,7 @@ message MutateOperation {
|
||||
DeletePermissionOperation delete_permission = 4;
|
||||
UpdateUserOrgRoleOperation update_user_org_role = 5;
|
||||
DeleteUserOrgRoleOperation delete_user_org_role = 6;
|
||||
AddUserOrgRoleOperation add_user_org_role = 7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +64,14 @@ message DeletePermissionOperation {
|
||||
Permission permission = 2;
|
||||
}
|
||||
|
||||
message AddUserOrgRoleOperation {
|
||||
// User UID
|
||||
string user = 1;
|
||||
// Role name (e.g: "Admin", "Editor", "Viewer")
|
||||
string role = 2;
|
||||
}
|
||||
|
||||
// UpdateUserOrgRoleOperation assigns the user's basic role and deletes existing basic role assignments.
|
||||
message UpdateUserOrgRoleOperation {
|
||||
// User UID
|
||||
string user = 1;
|
||||
|
||||
@@ -110,7 +110,7 @@ func ProvideStandaloneZanzanaClient(cfg *setting.Cfg, features featuremgmt.Featu
|
||||
ServerCertFile: cfg.ZanzanaClient.ServerCertFile,
|
||||
}
|
||||
|
||||
return NewRemoteZanzanaClient(fmt.Sprintf("stacks-%s", cfg.StackID), zanzanaConfig)
|
||||
return NewRemoteZanzanaClient(cfg.ZanzanaClient.TokenNamespace, zanzanaConfig)
|
||||
}
|
||||
|
||||
type ZanzanaClientConfig struct {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
authlib "github.com/grafana/authlib/types"
|
||||
|
||||
dashboards "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
@@ -23,6 +25,14 @@ var basicRolesTranslations = map[string]string{
|
||||
roleNone: "basic_none",
|
||||
}
|
||||
|
||||
var basicRolesUIDs = []string{
|
||||
"basic_grafana_admin",
|
||||
"basic_admin",
|
||||
"basic_editor",
|
||||
"basic_viewer",
|
||||
"basic_none",
|
||||
}
|
||||
|
||||
type resourceTranslation struct {
|
||||
typ string
|
||||
group string
|
||||
@@ -149,3 +159,7 @@ func TranslateToGroupResource(kind string) string {
|
||||
func TranslateBasicRole(name string) string {
|
||||
return basicRolesTranslations[name]
|
||||
}
|
||||
|
||||
func IsBasicRole(name string) bool {
|
||||
return slices.Contains(basicRolesUIDs, name)
|
||||
}
|
||||
|
||||
@@ -465,3 +465,21 @@ func AddRenderContext(req *openfgav1.CheckRequest) {
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
func SplitTupleObject(object string) (string, string, string) {
|
||||
var objectType, name, relation string
|
||||
parts := strings.Split(object, ":")
|
||||
if len(parts) < 2 {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
objectType = parts[0]
|
||||
nameRel := parts[1]
|
||||
parts = strings.Split(nameRel, "#")
|
||||
if len(parts) > 1 {
|
||||
relation = parts[1]
|
||||
}
|
||||
name = parts[0]
|
||||
|
||||
return objectType, name, relation
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func authorize(ctx context.Context, namespace string, ss setting.ZanzanaServerSe
|
||||
return status.Errorf(codes.Unauthenticated, "unauthenticated")
|
||||
}
|
||||
if !claims.NamespaceMatches(c.GetNamespace(), namespace) {
|
||||
return status.Errorf(codes.PermissionDenied, "namespace does not match")
|
||||
return status.Errorf(codes.PermissionDenied, "token namespace %s does not match request namespace", c.GetNamespace())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func getOperationGroup(operation *authzextv1.MutateOperation) (OperationGroup, e
|
||||
return OperationGroupFolder, nil
|
||||
case *authzextv1.MutateOperation_CreatePermission, *authzextv1.MutateOperation_DeletePermission:
|
||||
return OperationGroupPermission, nil
|
||||
case *authzextv1.MutateOperation_UpdateUserOrgRole, *authzextv1.MutateOperation_DeleteUserOrgRole:
|
||||
case *authzextv1.MutateOperation_UpdateUserOrgRole, *authzextv1.MutateOperation_DeleteUserOrgRole, *authzextv1.MutateOperation_AddUserOrgRole:
|
||||
return OperationGroupUserOrgRole, nil
|
||||
}
|
||||
return OperationGroup(""), errors.New("unsupported mutate operation type")
|
||||
|
||||
@@ -18,18 +18,29 @@ func (s *Server) mutateOrgRoles(ctx context.Context, store *storeInfo, operation
|
||||
|
||||
for _, operation := range operations {
|
||||
switch op := operation.Operation.(type) {
|
||||
case *authzextv1.MutateOperation_UpdateUserOrgRole:
|
||||
tuple, err := s.getUserOrgRoleWriteTuple(ctx, store, op.UpdateUserOrgRole)
|
||||
if err != nil {
|
||||
return err
|
||||
case *authzextv1.MutateOperation_AddUserOrgRole:
|
||||
basicRole := zanzana.TranslateBasicRole(op.AddUserOrgRole.GetRole())
|
||||
tuple := &openfgav1.TupleKey{
|
||||
User: zanzana.NewTupleEntry(zanzana.TypeUser, op.AddUserOrgRole.GetUser(), ""),
|
||||
Relation: zanzana.RelationAssignee,
|
||||
Object: zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, ""),
|
||||
}
|
||||
writeTuples = append(writeTuples, tuple)
|
||||
case *authzextv1.MutateOperation_DeleteUserOrgRole:
|
||||
tuple, err := s.getUserOrgRoleDeleteTuple(ctx, store, op.DeleteUserOrgRole)
|
||||
basicRole := zanzana.TranslateBasicRole(op.DeleteUserOrgRole.GetRole())
|
||||
tuple := &openfgav1.TupleKeyWithoutCondition{
|
||||
User: zanzana.NewTupleEntry(zanzana.TypeUser, op.DeleteUserOrgRole.GetUser(), ""),
|
||||
Relation: zanzana.RelationAssignee,
|
||||
Object: zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, ""),
|
||||
}
|
||||
deleteTuples = append(deleteTuples, tuple)
|
||||
case *authzextv1.MutateOperation_UpdateUserOrgRole:
|
||||
writeTuple, existingTuples, err := s.getUserOrgRoleUpdateTuples(ctx, store, op.UpdateUserOrgRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deleteTuples = append(deleteTuples, tuple)
|
||||
writeTuples = append(writeTuples, writeTuple)
|
||||
deleteTuples = append(deleteTuples, existingTuples...)
|
||||
default:
|
||||
s.logger.Debug("unsupported mutate operation", "operation", op)
|
||||
}
|
||||
@@ -65,20 +76,38 @@ func (s *Server) mutateOrgRoles(ctx context.Context, store *storeInfo, operation
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) getUserOrgRoleWriteTuple(ctx context.Context, store *storeInfo, req *authzextv1.UpdateUserOrgRoleOperation) (*openfgav1.TupleKey, error) {
|
||||
basicRole := zanzana.TranslateBasicRole(req.GetRole())
|
||||
return &openfgav1.TupleKey{
|
||||
User: zanzana.NewTupleEntry(zanzana.TypeUser, req.GetUser(), ""),
|
||||
Relation: zanzana.RelationAssignee,
|
||||
Object: zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, ""),
|
||||
}, nil
|
||||
}
|
||||
func (s *Server) getUserOrgRoleUpdateTuples(ctx context.Context, store *storeInfo, req *authzextv1.UpdateUserOrgRoleOperation) (*openfgav1.TupleKey, []*openfgav1.TupleKeyWithoutCondition, error) {
|
||||
readReq := &openfgav1.ReadRequest{
|
||||
StoreId: store.ID,
|
||||
TupleKey: &openfgav1.ReadRequestTupleKey{
|
||||
User: zanzana.NewTupleEntry(zanzana.TypeUser, req.GetUser(), ""),
|
||||
Relation: zanzana.RelationAssignee,
|
||||
// read tuples by object type ("role:")
|
||||
Object: zanzana.NewTupleEntry(zanzana.TypeRole, "", ""),
|
||||
},
|
||||
}
|
||||
res, err := s.openfga.Read(ctx, readReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
existingBasicRoleTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0)
|
||||
for _, tuple := range res.GetTuples() {
|
||||
_, roleName, _ := zanzana.SplitTupleObject(tuple.GetKey().GetObject())
|
||||
if zanzana.IsBasicRole(roleName) {
|
||||
existingBasicRoleTuples = append(existingBasicRoleTuples, &openfgav1.TupleKeyWithoutCondition{
|
||||
User: tuple.GetKey().GetUser(),
|
||||
Relation: tuple.GetKey().GetRelation(),
|
||||
Object: tuple.GetKey().GetObject(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) getUserOrgRoleDeleteTuple(ctx context.Context, store *storeInfo, req *authzextv1.DeleteUserOrgRoleOperation) (*openfgav1.TupleKeyWithoutCondition, error) {
|
||||
basicRole := zanzana.TranslateBasicRole(req.GetRole())
|
||||
return &openfgav1.TupleKeyWithoutCondition{
|
||||
writeTuple := &openfgav1.TupleKey{
|
||||
User: zanzana.NewTupleEntry(zanzana.TypeUser, req.GetUser(), ""),
|
||||
Relation: zanzana.RelationAssignee,
|
||||
Object: zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, ""),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return writeTuple, existingBasicRoleTuples, nil
|
||||
}
|
||||
|
||||
@@ -36,14 +36,6 @@ func testMutateOrgRoles(t *testing.T, srv *Server) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Operation: &v1.MutateOperation_DeleteUserOrgRole{
|
||||
DeleteUserOrgRole: &v1.DeleteUserOrgRoleOperation{
|
||||
User: "1",
|
||||
Role: "Editor",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -69,4 +61,50 @@ func testMutateOrgRoles(t *testing.T, srv *Server) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Tuples, 0)
|
||||
})
|
||||
|
||||
t.Run("should add user org role and delete old role", func(t *testing.T) {
|
||||
_, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{
|
||||
Namespace: "default",
|
||||
Operations: []*v1.MutateOperation{
|
||||
{
|
||||
Operation: &v1.MutateOperation_AddUserOrgRole{
|
||||
AddUserOrgRole: &v1.AddUserOrgRoleOperation{
|
||||
User: "1",
|
||||
Role: "Viewer",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Operation: &v1.MutateOperation_DeleteUserOrgRole{
|
||||
DeleteUserOrgRole: &v1.DeleteUserOrgRoleOperation{
|
||||
User: "1",
|
||||
Role: "Admin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{
|
||||
Namespace: "default",
|
||||
TupleKey: &v1.ReadRequestTupleKey{
|
||||
Relation: common.RelationAssignee,
|
||||
Object: "role:basic_admin",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Tuples, 0)
|
||||
|
||||
res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{
|
||||
Namespace: "default",
|
||||
TupleKey: &v1.ReadRequestTupleKey{
|
||||
Relation: common.RelationAssignee,
|
||||
Object: "role:basic_viewer",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Tuples, 1)
|
||||
require.Equal(t, "user:1", res.Tuples[0].Key.User)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1254,6 +1254,13 @@ var (
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaFrontendPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "timeRangePan",
|
||||
Description: "Enables time range panning functionality",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaDatavizSquad,
|
||||
},
|
||||
{
|
||||
Name: "azureMonitorDisableLogLimit",
|
||||
Description: "Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default.",
|
||||
@@ -2158,15 +2165,6 @@ var (
|
||||
Owner: grafanaSharingSquad,
|
||||
FrontendOnly: false,
|
||||
},
|
||||
{
|
||||
Name: "alertingMultiplePolicies",
|
||||
Description: "Enables the ability to create multiple alerting policies",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAlertingSquad,
|
||||
HideFromAdminPage: true,
|
||||
HideFromDocs: true,
|
||||
Expression: "false",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ managedDualWriter,experimental,@grafana/search-and-storage,false,false,false
|
||||
pluginsSriChecks,GA,@grafana/plugins-platform-backend,false,false,false
|
||||
unifiedStorageBigObjectsSupport,experimental,@grafana/search-and-storage,false,false,false
|
||||
timeRangeProvider,experimental,@grafana/grafana-frontend-platform,false,false,false
|
||||
timeRangePan,experimental,@grafana/dataviz-squad,false,false,true
|
||||
azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false
|
||||
preinstallAutoUpdate,GA,@grafana/plugins-platform-backend,false,false,false
|
||||
playlistsReconciler,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
@@ -277,4 +278,3 @@ pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,f
|
||||
onlyStoreActionSets,GA,@grafana/identity-access-team,false,false,false
|
||||
panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false
|
||||
dashboardTemplates,experimental,@grafana/sharing-squad,false,false,false
|
||||
alertingMultiplePolicies,experimental,@grafana/alerting-squad,false,false,false
|
||||
|
||||
|
8
pkg/services/featuremgmt/toggles_gen.go
generated
8
pkg/services/featuremgmt/toggles_gen.go
generated
@@ -667,6 +667,10 @@ const (
|
||||
// Enables time pickers sync
|
||||
FlagTimeRangeProvider = "timeRangeProvider"
|
||||
|
||||
// FlagTimeRangePan
|
||||
// Enables time range panning functionality
|
||||
FlagTimeRangePan = "timeRangePan"
|
||||
|
||||
// FlagAzureMonitorDisableLogLimit
|
||||
// Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default.
|
||||
FlagAzureMonitorDisableLogLimit = "azureMonitorDisableLogLimit"
|
||||
@@ -1117,8 +1121,4 @@ const (
|
||||
// FlagDashboardTemplates
|
||||
// Enable template dashboards
|
||||
FlagDashboardTemplates = "dashboardTemplates"
|
||||
|
||||
// FlagAlertingMultiplePolicies
|
||||
// Enables the ability to create multiple alerting policies
|
||||
FlagAlertingMultiplePolicies = "alertingMultiplePolicies"
|
||||
)
|
||||
|
||||
@@ -405,21 +405,6 @@
|
||||
"expression": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingMultiplePolicies",
|
||||
"resourceVersion": "1762186293341",
|
||||
"creationTimestamp": "2025-11-03T16:11:33Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables the ability to create multiple alerting policies",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/alerting-squad",
|
||||
"hideFromAdminPage": true,
|
||||
"hideFromDocs": true,
|
||||
"expression": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingNotificationHistory",
|
||||
@@ -3930,6 +3915,22 @@
|
||||
"frontend": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "timeRangePan",
|
||||
"resourceVersion": "1762290731154",
|
||||
"creationTimestamp": "2025-10-24T19:49:53Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-11-04 21:12:11.154822 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables time range panning functionality",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/dataviz-squad",
|
||||
"frontend": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "timeRangeProvider",
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/api/hcl"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -59,9 +58,6 @@ type NotificationPolicyService interface {
|
||||
GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, string, error)
|
||||
UpdatePolicyTree(ctx context.Context, orgID int64, tree definitions.Route, p alerting_models.Provenance, version string) (definitions.Route, string, error)
|
||||
ResetPolicyTree(ctx context.Context, orgID int64, provenance alerting_models.Provenance) (definitions.Route, error)
|
||||
|
||||
GetManagedRoute(ctx context.Context, orgID int64, name string) (legacy_storage.ManagedRoute, error)
|
||||
GetManagedRoutes(ctx context.Context, orgID int64) (legacy_storage.ManagedRoutes, error)
|
||||
}
|
||||
|
||||
type MuteTimingService interface {
|
||||
@@ -100,43 +96,19 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) respo
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RouteGetPolicyTreeExport(c *contextmodel.ReqContext) response.Response {
|
||||
if !srv.featureManager.IsEnabledGlobally(featuremgmt.FlagAlertingMultiplePolicies) {
|
||||
// Default to the old behavior of exporting the single user-defined policy tree without a "name" field.
|
||||
policy, _, err := srv.policies.GetPolicyTree(c.Req.Context(), c.GetOrgID())
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
||||
return ErrResp(http.StatusNotFound, err, "")
|
||||
}
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
policies, _, err := srv.policies.GetPolicyTree(c.Req.Context(), c.GetOrgID())
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
|
||||
return ErrResp(http.StatusNotFound, err, "")
|
||||
}
|
||||
e, err := AlertingFileExportFromRoute(c.GetOrgID(), policy)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export")
|
||||
}
|
||||
return exportResponse(c, e)
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
|
||||
routeName := c.Query("routeName")
|
||||
var routesToExport legacy_storage.ManagedRoutes
|
||||
if routeName == "" {
|
||||
// Interpreted as Export All.
|
||||
var err error
|
||||
routesToExport, err = srv.policies.GetManagedRoutes(c.Req.Context(), c.GetOrgID())
|
||||
if err != nil {
|
||||
return response.ErrOrFallback(http.StatusInternalServerError, "failed to export all notification policy trees", err)
|
||||
}
|
||||
} else {
|
||||
managedRoute, err := srv.policies.GetManagedRoute(c.Req.Context(), c.GetOrgID(), routeName)
|
||||
if err != nil {
|
||||
return response.ErrOrFallback(http.StatusInternalServerError, "failed to export notification policy tree", err)
|
||||
}
|
||||
routesToExport = legacy_storage.ManagedRoutes{&managedRoute}
|
||||
}
|
||||
|
||||
e, err := AlertingFileExportFromManagedRoutes(c.GetOrgID(), routesToExport)
|
||||
e, err := AlertingFileExportFromRoute(c.GetOrgID(), policies)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "failed to create alerting file export")
|
||||
}
|
||||
|
||||
return exportResponse(c, e)
|
||||
}
|
||||
|
||||
@@ -621,9 +593,6 @@ func escapeAlertingFileExport(body definitions.AlertingFileExport) definitions.A
|
||||
}
|
||||
|
||||
func escapeRouteExport(r *definitions.RouteExport) {
|
||||
if r.Name != nil {
|
||||
r.Name = util.Pointer(addEscapeCharactersToString(*r.Name))
|
||||
}
|
||||
r.Receiver = addEscapeCharactersToString(r.Receiver)
|
||||
if r.GroupByStr != nil {
|
||||
groupByStr := make([]string, len(*r.GroupByStr))
|
||||
|
||||
@@ -2248,7 +2248,6 @@ func createTestRequestCtx() contextmodel.ReqContext {
|
||||
}
|
||||
|
||||
type fakeNotificationPolicyService struct {
|
||||
NotificationPolicyService
|
||||
tree definitions.Route
|
||||
prov models.Provenance
|
||||
}
|
||||
@@ -2319,9 +2318,7 @@ func (f *fakeNotificationPolicyService) ResetPolicyTree(ctx context.Context, org
|
||||
return f.tree, nil
|
||||
}
|
||||
|
||||
type fakeFailingNotificationPolicyService struct {
|
||||
NotificationPolicyService
|
||||
}
|
||||
type fakeFailingNotificationPolicyService struct{}
|
||||
|
||||
func (f *fakeFailingNotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, string, error) {
|
||||
return definitions.Route{}, "", fmt.Errorf("something went wrong")
|
||||
@@ -2335,9 +2332,7 @@ func (f *fakeFailingNotificationPolicyService) ResetPolicyTree(ctx context.Conte
|
||||
return definitions.Route{}, fmt.Errorf("something went wrong")
|
||||
}
|
||||
|
||||
type fakeRejectingNotificationPolicyService struct {
|
||||
NotificationPolicyService
|
||||
}
|
||||
type fakeRejectingNotificationPolicyService struct{}
|
||||
|
||||
type fakeRejectingNotificationSettingsValidatorProvider struct{}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@@ -339,21 +338,6 @@ func AlertingFileExportFromRoute(orgID int64, route definitions.Route) (definiti
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// AlertingFileExportFromManagedRoutes creates a definitions.AlertingFileExport DTO from one or more legacy_storage.ManagedRoute.
|
||||
func AlertingFileExportFromManagedRoutes(orgID int64, routes legacy_storage.ManagedRoutes) (definitions.AlertingFileExport, error) {
|
||||
f := definitions.AlertingFileExport{
|
||||
APIVersion: 1,
|
||||
Policies: make([]definitions.NotificationPolicyExport, 0, len(routes)),
|
||||
}
|
||||
for _, route := range routes {
|
||||
f.Policies = append(f.Policies, definitions.NotificationPolicyExport{
|
||||
OrgID: orgID,
|
||||
RouteExport: RouteExportFromManagedRoute(route),
|
||||
})
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// RouteExportFromRoute creates a definitions.RouteExport DTO from definitions.Route.
|
||||
func RouteExportFromRoute(route *definitions.Route) *definitions.RouteExport {
|
||||
toStringIfNotNil := func(d *model.Duration) *string {
|
||||
@@ -399,18 +383,6 @@ func RouteExportFromRoute(route *definitions.Route) *definitions.RouteExport {
|
||||
return &export
|
||||
}
|
||||
|
||||
// RouteExportFromManagedRoute creates a definitions.RouteExport DTO from legacy_storage.ManagedRoute.
|
||||
func RouteExportFromManagedRoute(route *legacy_storage.ManagedRoute) *definitions.RouteExport {
|
||||
apiRoute := route.AsRoute()
|
||||
export := RouteExportFromRoute(&apiRoute)
|
||||
if route.Name == legacy_storage.UserDefinedRoutingTreeName {
|
||||
export.Name = nil // Functionally this shouldn't matter, aesthetically this prefers an empty name over "user-defined".
|
||||
} else {
|
||||
export.Name = OmitDefault(util.Pointer(route.Name))
|
||||
}
|
||||
return export
|
||||
}
|
||||
|
||||
// OmitDefault returns nil if the value is the default.
|
||||
func OmitDefault[T comparable](v *T) *T {
|
||||
var def T
|
||||
|
||||
@@ -775,11 +775,10 @@ func fromPrometheusConfig(prometheusConfig config.Config) PostableApiAlertingCon
|
||||
|
||||
// swagger:model
|
||||
type PostableUserConfig struct {
|
||||
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
|
||||
AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
|
||||
ExtraConfigs []ExtraConfiguration `yaml:"extra_config,omitempty" json:"extra_config,omitempty"`
|
||||
ManagedRoutes map[string]*definition.Route `yaml:"managed_routes,omitempty" json:"managed_routes,omitempty"` // TODO: Move to ConfigRevision?
|
||||
amSimple map[string]interface{} `yaml:"-" json:"-"`
|
||||
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
|
||||
AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
|
||||
ExtraConfigs []ExtraConfiguration `yaml:"extra_config,omitempty" json:"extra_config,omitempty"`
|
||||
amSimple map[string]interface{} `yaml:"-" json:"-"`
|
||||
}
|
||||
|
||||
func (c *PostableUserConfig) GetMergedAlertmanagerConfig() (MergeResult, error) {
|
||||
|
||||
@@ -70,8 +70,7 @@ type NotificationPolicyExport struct {
|
||||
// RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't useable in
|
||||
// provisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them.
|
||||
type RouteExport struct {
|
||||
Name *string `yaml:"name,omitempty" json:"name,omitempty" hcl:"name,optional"`
|
||||
Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"contact_point"`
|
||||
Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"contact_point"`
|
||||
|
||||
GroupByStr *[]string `yaml:"group_by,omitempty" json:"group_by,omitempty" hcl:"group_by"`
|
||||
// Deprecated. Remove before v1.0 release.
|
||||
|
||||
@@ -422,7 +422,7 @@ func (ng *AlertNG) init() error {
|
||||
)
|
||||
|
||||
// Provisioning
|
||||
policyService := provisioning.NewNotificationPolicyService(configStore, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.FeatureToggles, ng.Log)
|
||||
policyService := provisioning.NewNotificationPolicyService(configStore, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.Log)
|
||||
contactPointService := provisioning.NewContactPointService(configStore, ng.SecretsService, ng.store, ng.store, provisioningReceiverService, ng.Log, ng.store, ng.ResourcePermissions)
|
||||
templateService := provisioning.NewTemplateService(configStore, ng.store, ng.store, ng.Log)
|
||||
muteTimingService := provisioning.NewMuteTimingService(configStore, ng.store, ng.store, ng.Log, ng.store)
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -337,9 +336,6 @@ func (am *alertmanager) applyConfig(ctx context.Context, cfg *apimodels.Postable
|
||||
return false, fmt.Errorf("failed to decrypt external configurations: %w", err)
|
||||
}
|
||||
|
||||
// Add managed routes to the configuration.
|
||||
cfg.AlertmanagerConfig.Route = legacy_storage.WithManagedRoutes(cfg.AlertmanagerConfig.Route, cfg.ManagedRoutes)
|
||||
|
||||
mergeResult, err := cfg.GetMergedAlertmanagerConfig()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get full alertmanager configuration: %w", err)
|
||||
|
||||
@@ -12,12 +12,6 @@ var (
|
||||
"Invalid receiver: '{{ .Public.Reason }}'",
|
||||
errutil.WithPublic("Invalid receiver: '{{ .Public.Reason }}'"),
|
||||
)
|
||||
|
||||
ErrRouteExists = errutil.Conflict("alerting.notifications.routes.exists", errutil.WithPublicMessage("Route with this name already exists. Use a different name or update an existing one."))
|
||||
ErrRouteInvalidFormat = errutil.BadRequest("alerting.notifications.routes.invalidFormat").MustTemplate(
|
||||
"Invalid format of the submitted route.",
|
||||
errutil.WithPublic("Invalid format of the submitted route: {{.Public.Error}}. Correct the payload and try again."),
|
||||
)
|
||||
)
|
||||
|
||||
func makeErrBadAlertmanagerConfiguration(err error) error {
|
||||
@@ -39,12 +33,3 @@ func MakeErrReceiverInvalid(err error) error {
|
||||
}
|
||||
return ErrReceiverInvalid.Build(data)
|
||||
}
|
||||
|
||||
func MakeErrRouteInvalidFormat(err error) error {
|
||||
return ErrRouteInvalidFormat.Build(errutil.TemplateData{
|
||||
Public: map[string]any{
|
||||
"Error": err.Error(),
|
||||
},
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,380 +0,0 @@
|
||||
package legacy_storage
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"hash"
|
||||
"hash/fnv"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/grafana/alerting/definition"
|
||||
"github.com/prometheus/alertmanager/dispatch"
|
||||
"github.com/prometheus/alertmanager/pkg/labels"
|
||||
"github.com/prometheus/common/model"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
|
||||
const UserDefinedRoutingTreeName = "user-defined"
|
||||
const NamedRouteMatcher = "__grafana_managed_route__"
|
||||
|
||||
type ManagedRoute struct {
|
||||
Name string
|
||||
Version string
|
||||
|
||||
Receiver string
|
||||
GroupBy []string
|
||||
GroupWait *model.Duration
|
||||
GroupInterval *model.Duration
|
||||
RepeatInterval *model.Duration
|
||||
Routes []*definition.Route
|
||||
|
||||
Provenance models.Provenance
|
||||
}
|
||||
|
||||
func (r *ManagedRoute) AsRoute() definition.Route {
|
||||
// Only need to copy the fields that are valid for a root route.
|
||||
return definition.Route{
|
||||
Receiver: r.Receiver,
|
||||
GroupByStr: r.GroupBy,
|
||||
GroupWait: r.GroupWait,
|
||||
GroupInterval: r.GroupInterval,
|
||||
RepeatInterval: r.RepeatInterval,
|
||||
Routes: r.Routes,
|
||||
Provenance: definitions.Provenance(r.Provenance),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ManagedRoute) GeneratedSubRoute() *definition.Route {
|
||||
amRoute := r.AsRoute()
|
||||
|
||||
// It's important that the generated sub-route is fully defined so that they will never rely on the values of the root.
|
||||
defaultOpts := dispatch.DefaultRouteOpts
|
||||
if amRoute.GroupWait == nil {
|
||||
gw := model.Duration(defaultOpts.GroupWait)
|
||||
amRoute.GroupWait = &gw
|
||||
}
|
||||
if amRoute.GroupInterval == nil {
|
||||
gi := model.Duration(defaultOpts.GroupInterval)
|
||||
amRoute.GroupInterval = &gi
|
||||
}
|
||||
if amRoute.RepeatInterval == nil {
|
||||
ri := model.Duration(defaultOpts.RepeatInterval)
|
||||
amRoute.RepeatInterval = &ri
|
||||
}
|
||||
if r.Name != UserDefinedRoutingTreeName {
|
||||
// Set label matcher.
|
||||
amRoute.ObjectMatchers = definitions.ObjectMatchers{managedRouteMatcher(r.Name)}
|
||||
}
|
||||
return &amRoute
|
||||
}
|
||||
|
||||
func (r *ManagedRoute) ResourceType() string {
|
||||
return (&definition.Route{}).ResourceType()
|
||||
}
|
||||
|
||||
func (r *ManagedRoute) ResourceID() string {
|
||||
if r.Name == UserDefinedRoutingTreeName {
|
||||
// Backwards compatibility with legacy user-defined routing tree.
|
||||
return ""
|
||||
}
|
||||
return r.Name
|
||||
}
|
||||
|
||||
func NewManagedRoute(name string, r *definition.Route) *ManagedRoute {
|
||||
return &ManagedRoute{
|
||||
Name: name,
|
||||
Version: CalculateRouteFingerprint(*r),
|
||||
|
||||
Receiver: r.Receiver,
|
||||
GroupBy: r.GroupByStr,
|
||||
GroupWait: r.GroupWait,
|
||||
GroupInterval: r.GroupInterval,
|
||||
RepeatInterval: r.RepeatInterval,
|
||||
Routes: r.Routes,
|
||||
|
||||
Provenance: models.Provenance(r.Provenance),
|
||||
}
|
||||
}
|
||||
|
||||
func managedRouteMatcher(name string) *labels.Matcher {
|
||||
return &labels.Matcher{
|
||||
Type: labels.MatchEqual,
|
||||
Name: NamedRouteMatcher,
|
||||
Value: name,
|
||||
}
|
||||
}
|
||||
|
||||
type ManagedRoutes []*ManagedRoute
|
||||
|
||||
func (m ManagedRoutes) Sort() {
|
||||
// Sort the keys of the map to ensure consistent ordering. Always ensure that the legacy user-defined routing tree is last.
|
||||
slices.SortFunc(m, func(a, b *ManagedRoute) int {
|
||||
if a.Name == UserDefinedRoutingTreeName {
|
||||
return 1
|
||||
}
|
||||
if b.Name == UserDefinedRoutingTreeName {
|
||||
return -1
|
||||
}
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func WithManagedRoutes(root *definitions.Route, managedRoutes map[string]*definition.Route) *definitions.Route {
|
||||
if len(managedRoutes) == 0 {
|
||||
// If there are no managed routes, we just return the original root.
|
||||
return root
|
||||
}
|
||||
newRoot := *root
|
||||
newManagedRoutes := make([]*definition.Route, 0, len(newRoot.Routes)+len(managedRoutes))
|
||||
for _, k := range slices.Sorted(maps.Keys(managedRoutes)) {
|
||||
// On the off chance that the route is nil or invalid managed route with the restricted name, we skip it.
|
||||
if managedRoutes[k] == nil || k == UserDefinedRoutingTreeName {
|
||||
continue
|
||||
}
|
||||
newManagedRoutes = append(newManagedRoutes, NewManagedRoute(k, managedRoutes[k]).GeneratedSubRoute())
|
||||
}
|
||||
|
||||
// Add the user-defined routing tree at the end.
|
||||
newManagedRoutes = append(newManagedRoutes, newRoot.Routes...)
|
||||
newRoot.Routes = newManagedRoutes
|
||||
return &newRoot
|
||||
}
|
||||
|
||||
func (rev *ConfigRevision) GetManagedRoute(name string) *ManagedRoute {
|
||||
if name == UserDefinedRoutingTreeName {
|
||||
return NewManagedRoute(UserDefinedRoutingTreeName, rev.Config.AlertmanagerConfig.Route)
|
||||
}
|
||||
route, ok := rev.Config.ManagedRoutes[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return NewManagedRoute(name, route)
|
||||
}
|
||||
|
||||
func (rev *ConfigRevision) GetManagedRoutes() ManagedRoutes {
|
||||
managedRoutes := make(ManagedRoutes, 0, len(rev.Config.ManagedRoutes)+1)
|
||||
for _, k := range slices.Sorted(maps.Keys(rev.Config.ManagedRoutes)) {
|
||||
// On the off chance that the route is nil or invalid managed route with the restricted name, we skip it.
|
||||
if rev.Config.ManagedRoutes[k] == nil || k == UserDefinedRoutingTreeName {
|
||||
continue
|
||||
}
|
||||
managedRoutes = append(managedRoutes, NewManagedRoute(k, rev.Config.ManagedRoutes[k]))
|
||||
}
|
||||
|
||||
managedRoutes = append(managedRoutes, NewManagedRoute(UserDefinedRoutingTreeName, rev.Config.AlertmanagerConfig.Route))
|
||||
|
||||
return managedRoutes
|
||||
}
|
||||
|
||||
func (rev *ConfigRevision) DeleteManagedRoute(name string) {
|
||||
// Intentionally does not consider if name == UserDefinedRoutingTreeName as it should only be Reset via Update.
|
||||
delete(rev.Config.ManagedRoutes, name)
|
||||
}
|
||||
|
||||
func (rev *ConfigRevision) CreateManagedRoute(name string, subtree definitions.Route) (*ManagedRoute, error) {
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("route name is required")
|
||||
}
|
||||
|
||||
if name == UserDefinedRoutingTreeName {
|
||||
return nil, fmt.Errorf("cannot create a managed route with the name %q, this name is reserved for the user-defined routing tree", UserDefinedRoutingTreeName)
|
||||
}
|
||||
|
||||
if _, exists := rev.Config.ManagedRoutes[name]; exists {
|
||||
return nil, ErrRouteExists.Errorf("")
|
||||
}
|
||||
|
||||
managedRoute := NewManagedRoute(name, &subtree)
|
||||
amRoute := managedRoute.AsRoute()
|
||||
|
||||
err := rev.ValidateRoute(amRoute)
|
||||
if err != nil {
|
||||
return nil, MakeErrRouteInvalidFormat(err)
|
||||
}
|
||||
|
||||
if rev.Config.ManagedRoutes == nil {
|
||||
rev.Config.ManagedRoutes = make(map[string]*definition.Route, 1)
|
||||
}
|
||||
rev.Config.ManagedRoutes[name] = &amRoute
|
||||
|
||||
return managedRoute, nil
|
||||
}
|
||||
|
||||
func (rev *ConfigRevision) UpdateNamedRoute(name string, subtree definitions.Route) (*ManagedRoute, error) {
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("route name is required")
|
||||
}
|
||||
|
||||
if existing := rev.GetManagedRoute(name); existing == nil {
|
||||
return nil, fmt.Errorf("managed route %q not found", name)
|
||||
}
|
||||
|
||||
managedRoute := NewManagedRoute(name, &subtree)
|
||||
amRoute := managedRoute.AsRoute()
|
||||
|
||||
err := rev.ValidateRoute(amRoute)
|
||||
if err != nil {
|
||||
return nil, MakeErrRouteInvalidFormat(err)
|
||||
}
|
||||
|
||||
if name == UserDefinedRoutingTreeName {
|
||||
rev.Config.AlertmanagerConfig.Route = &amRoute
|
||||
} else {
|
||||
if rev.Config.ManagedRoutes == nil {
|
||||
rev.Config.ManagedRoutes = make(map[string]*definition.Route, 1)
|
||||
}
|
||||
rev.Config.ManagedRoutes[name] = &amRoute
|
||||
}
|
||||
|
||||
return managedRoute, nil
|
||||
}
|
||||
|
||||
func (rev *ConfigRevision) ResetUserDefinedRoute(defaultCfg *definitions.PostableUserConfig) (*ManagedRoute, error) {
|
||||
// Ensure the new default receiver exists and if not, create it.
|
||||
if err := rev.validateReceiverReferences(*defaultCfg.AlertmanagerConfig.Route); err != nil {
|
||||
// Default receiver doesn't exist, create it.
|
||||
var defaultRcv *definitions.PostableApiReceiver
|
||||
for _, rcv := range defaultCfg.AlertmanagerConfig.Receivers {
|
||||
if rcv.Name == defaultCfg.AlertmanagerConfig.Route.Receiver {
|
||||
defaultRcv = rcv
|
||||
break
|
||||
}
|
||||
}
|
||||
if defaultRcv == nil {
|
||||
return nil, fmt.Errorf("inconsistent default configuration: default receiver %q not found", defaultCfg.AlertmanagerConfig.Route.Receiver)
|
||||
}
|
||||
rev.Config.AlertmanagerConfig.Receivers = append(rev.Config.AlertmanagerConfig.Receivers, defaultRcv)
|
||||
}
|
||||
|
||||
return rev.UpdateNamedRoute(UserDefinedRoutingTreeName, *defaultCfg.AlertmanagerConfig.Route)
|
||||
}
|
||||
|
||||
func (rev *ConfigRevision) ValidateRoute(route definitions.Route) error {
|
||||
err := route.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rev.validateReceiverReferences(route)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rev.validateTimeIntervalReferences(route)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rev *ConfigRevision) validateReceiverReferences(route definitions.Route) error {
|
||||
receivers := rev.GetReceiversNames()
|
||||
receivers[""] = struct{}{} // Allow empty receiver (inheriting from parent)
|
||||
return route.ValidateReceivers(receivers)
|
||||
}
|
||||
|
||||
func (rev *ConfigRevision) validateTimeIntervalReferences(route definitions.Route) error {
|
||||
timeIntervals := map[string]struct{}{}
|
||||
for _, mt := range rev.Config.AlertmanagerConfig.MuteTimeIntervals {
|
||||
timeIntervals[mt.Name] = struct{}{}
|
||||
}
|
||||
for _, mt := range rev.Config.AlertmanagerConfig.TimeIntervals {
|
||||
timeIntervals[mt.Name] = struct{}{}
|
||||
}
|
||||
return route.ValidateTimeIntervals(timeIntervals)
|
||||
}
|
||||
|
||||
func CalculateRouteFingerprint(route definitions.Route) string {
|
||||
sum := fnv.New64a()
|
||||
writeToHash(sum, &route)
|
||||
return fmt.Sprintf("%016x", sum.Sum64())
|
||||
}
|
||||
|
||||
func writeToHash(sum hash.Hash, r *definitions.Route) {
|
||||
writeBytes := func(b []byte) {
|
||||
_, _ = sum.Write(b)
|
||||
// add a byte sequence that cannot happen in UTF-8 strings.
|
||||
_, _ = sum.Write([]byte{255})
|
||||
}
|
||||
writeString := func(s string) {
|
||||
if len(s) == 0 {
|
||||
writeBytes(nil)
|
||||
return
|
||||
}
|
||||
// #nosec G103
|
||||
// avoid allocation when converting string to byte slice
|
||||
writeBytes(unsafe.Slice(unsafe.StringData(s), len(s)))
|
||||
}
|
||||
|
||||
// this temp slice is used to convert ints to bytes.
|
||||
tmp := make([]byte, 8)
|
||||
writeInt := func(u int64) {
|
||||
binary.LittleEndian.PutUint64(tmp, uint64(u))
|
||||
writeBytes(tmp)
|
||||
}
|
||||
writeBool := func(b bool) {
|
||||
if b {
|
||||
writeInt(1)
|
||||
} else {
|
||||
writeInt(0)
|
||||
}
|
||||
}
|
||||
writeDuration := func(d *model.Duration) {
|
||||
if d == nil {
|
||||
_, _ = sum.Write([]byte{255})
|
||||
} else {
|
||||
binary.LittleEndian.PutUint64(tmp, uint64(*d))
|
||||
_, _ = sum.Write(tmp)
|
||||
_, _ = sum.Write([]byte{255})
|
||||
}
|
||||
}
|
||||
|
||||
writeString(r.Receiver)
|
||||
for _, s := range r.GroupByStr {
|
||||
writeString(s)
|
||||
}
|
||||
for _, labelName := range r.GroupBy {
|
||||
writeString(string(labelName))
|
||||
}
|
||||
writeBool(r.GroupByAll)
|
||||
if len(r.Match) > 0 {
|
||||
for _, key := range slices.Sorted(maps.Keys(r.Match)) {
|
||||
writeString(key)
|
||||
writeString(r.Match[key])
|
||||
}
|
||||
}
|
||||
if len(r.MatchRE) > 0 {
|
||||
for _, key := range slices.Sorted(maps.Keys(r.MatchRE)) {
|
||||
writeString(key)
|
||||
str, err := r.MatchRE[key].MarshalJSON()
|
||||
if err != nil {
|
||||
writeString(fmt.Sprintf("%+v", r.MatchRE))
|
||||
}
|
||||
writeBytes(str)
|
||||
}
|
||||
}
|
||||
for _, matcher := range r.Matchers {
|
||||
writeString(matcher.String())
|
||||
}
|
||||
for _, matcher := range r.ObjectMatchers {
|
||||
writeString(matcher.String())
|
||||
}
|
||||
for _, timeInterval := range r.MuteTimeIntervals {
|
||||
writeString(timeInterval)
|
||||
}
|
||||
for _, timeInterval := range r.ActiveTimeIntervals {
|
||||
writeString(timeInterval)
|
||||
}
|
||||
writeBool(r.Continue)
|
||||
writeDuration(r.GroupWait)
|
||||
writeDuration(r.GroupInterval)
|
||||
writeDuration(r.RepeatInterval)
|
||||
for _, route := range r.Routes {
|
||||
writeToHash(sum, route)
|
||||
}
|
||||
}
|
||||
@@ -41,8 +41,6 @@ var (
|
||||
ErrRouteConflictingMatchers = errutil.BadRequest("alerting.notifications.routes.conflictingMatchers").MustTemplate("Routing tree conflicts with the external configuration",
|
||||
errutil.WithPublic("Cannot add\\update route: matchers conflict with an external routing tree merging matchers {{ .Public.Matchers }}, making the added\\updated route unreachable."),
|
||||
)
|
||||
|
||||
ErrRouteNotFound = errutil.NotFound("alerting.notifications.routes.notFound", errutil.WithPublicMessage("Route not found"))
|
||||
)
|
||||
|
||||
// MakeErrTimeIntervalInvalid creates an error with the ErrTimeIntervalInvalid template
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user