Repository: prometheus-community/prom-label-proxy Branch: main Commit: bab0fc7ea168 Files: 62 Total size: 275.8 KB Directory structure: gitextract_w6i2li4x/ ├── .dockerignore ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── container_description.yml │ ├── golangci-lint.yml │ └── govulncheck.yml ├── .gitignore ├── .golangci.yml ├── .promu.yml ├── .yamllint ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── Makefile.common ├── OWNERS ├── README.md ├── SECURITY.md ├── VERSION ├── dependabot.yml ├── examples/ │ ├── caddy-port-based/ │ │ ├── Caddyfile │ │ └── README.md │ └── kube-rbac-proxy/ │ ├── Dockerfile │ ├── client.yaml │ ├── deployment.yaml │ └── rbac.yaml ├── go.mod ├── go.sum ├── injectproxy/ │ ├── alerts.go │ ├── alerts_test.go │ ├── enforce.go │ ├── enforce_test.go │ ├── routes.go │ ├── routes_test.go │ ├── rules.go │ ├── rules_test.go │ ├── silences.go │ ├── silences_test.go │ ├── testdata/ │ │ ├── alerts_incomplete_upstream_response.golden │ │ ├── alerts_invalid_upstream_response.golden │ │ ├── alerts_match_namespace_ns1.golden │ │ ├── alerts_match_namespace_ns2.golden │ │ ├── alerts_match_namespaces_ns1_and_ns2.golden │ │ ├── alerts_no_match.golden │ │ ├── alerts_no_namespace_error.golden │ │ ├── alerts_upstream_error.golden │ │ ├── rules_incomplete_upstream_response.golden │ │ ├── rules_invalid_upstream_response.golden │ │ ├── rules_match_namespace_ns1.golden │ │ ├── rules_match_namespace_ns2.golden │ │ ├── rules_match_namespaces_ns1_and_ns2.golden │ │ ├── rules_no_match.golden │ │ ├── rules_no_match_with_gzip_not_requested.golden │ │ ├── rules_no_match_with_gzip_requested.golden │ │ ├── rules_no_namespace_error.golden │ │ ├── rules_upstream_error.golden │ │ ├── rules_with_active_alerts.golden │ │ └── rules_with_label_matchers.golden │ └── utils.go └── main.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ data/ .build/ .tarballs/ !.build/linux-amd64/ !.build/linux-arm64/ !.build/linux-armv7/ !.build/linux-ppc64le/ !.build/linux-riscv64/ !.build/linux-s390x/ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "monthly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" # Exclude configs synced from upstream prometheus/prometheus. exclude-paths: - .github/workflows/container_description.yml - .github/workflows/golangci-lint.yml ================================================ FILE: .github/workflows/ci.yml ================================================ --- name: CI on: pull_request: push: branches: [main, master, 'release-*'] tags: ['v*'] permissions: contents: read jobs: test_go: name: Go tests runs-on: ubuntu-latest container: # Whenever the Go version is updated here, .promu.yml # should also be updated. image: quay.io/prometheus/golang-builder:1.26-base steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: prometheus/promci-setup@5af30ba8c199a91d6c04ebdc3c48e630e355f62d # v0.1.0 - run: make GO_ONLY=1 SKIP_GOLANGCI_LINT=1 build: name: Build runs-on: ubuntu-latest strategy: matrix: thread: [ 0, 1, 2, 3] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: prometheus/promci/build@9c86752f3395e08c57719af549cc455d8e2c2514 # v0.7.0 with: parallelism: 4 thread: ${{ matrix.thread }} publish_main: name: Publish main branch artifacts runs-on: ubuntu-latest needs: [test_go, build] if: | (github.event_name == 'push' && github.event.ref == 'refs/heads/main') || (github.event_name == 'push' && github.event.ref == 'refs/heads/master') steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: prometheus/promci/publish_main@9c86752f3395e08c57719af549cc455d8e2c2514 # v0.7.0 with: docker_hub_organization: prometheuscommunity docker_hub_password: ${{ secrets.docker_hub_password }} quay_io_organization: prometheuscommunity quay_io_password: ${{ secrets.quay_io_password }} publish_release: name: Publish release artefacts runs-on: ubuntu-latest needs: [test_go, build] if: | (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: prometheus/promci/publish_release@9c86752f3395e08c57719af549cc455d8e2c2514 # v0.7.0 with: docker_hub_organization: prometheuscommunity docker_hub_password: ${{ secrets.docker_hub_password }} quay_io_organization: prometheuscommunity quay_io_password: ${{ secrets.quay_io_password }} github_token: ${{ secrets.PROMBOT_GITHUB_TOKEN }} ================================================ FILE: .github/workflows/container_description.yml ================================================ --- name: Push README to Docker Hub on: push: paths: - "README.md" - "README-containers.md" - ".github/workflows/container_description.yml" branches: [ main, master ] permissions: contents: read jobs: PushDockerHubReadme: runs-on: ubuntu-latest name: Push README to Docker Hub if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. steps: - name: git checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set docker hub repo name run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV - name: Push README to Dockerhub uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 env: DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }} DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }} with: destination_container_repo: ${{ env.DOCKER_REPO_NAME }} provider: dockerhub short_description: ${{ env.DOCKER_REPO_NAME }} # Empty string results in README-containers.md being pushed if it # exists. Otherwise, README.md is pushed. readme_file: '' PushQuayIoReadme: runs-on: ubuntu-latest name: Push README to quay.io if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. steps: - name: git checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set quay.io org name run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV - name: Set quay.io repo name run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV - name: Push README to quay.io uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 env: DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }} with: destination_container_repo: ${{ env.DOCKER_REPO_NAME }} provider: quay # Empty string results in README-containers.md being pushed if it # exists. Otherwise, README.md is pushed. readme_file: '' ================================================ FILE: .github/workflows/golangci-lint.yml ================================================ --- # This action is synced from https://github.com/prometheus/prometheus name: golangci-lint on: push: branches: [main, master, 'release-*'] paths: - "go.sum" - "go.mod" - "**.go" - "scripts/errcheck_excludes.txt" - ".github/workflows/golangci-lint.yml" - ".golangci.yml" tags: ['v*'] pull_request: permissions: # added using https://github.com/step-security/secure-repo contents: read jobs: golangci: permissions: contents: read # for actions/checkout to fetch code pull-requests: read # for golangci/golangci-lint-action to fetch pull requests name: lint runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Install Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: 1.26.x - name: Install snmp_exporter/generator dependencies run: sudo apt-get update && sudo apt-get -y install libsnmp-dev if: github.repository == 'prometheus/snmp_exporter' - name: Get golangci-lint version id: golangci-lint-version run: echo "version=$(make print-golangci-lint-version)" >> $GITHUB_OUTPUT - name: Lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: args: --verbose version: ${{ steps.golangci-lint-version.outputs.version }} ================================================ FILE: .github/workflows/govulncheck.yml ================================================ --- name: govulncheck on: pull_request: push: branches: - main - master schedule: - cron: '33 2 * * *' permissions: contents: read jobs: govulncheck: runs-on: ubuntu-latest name: Run govulncheck steps: - id: govulncheck uses: golang/govulncheck-action@31f7c5463448f83528bd771c2d978d940080c9fd # v1.0.4-unreleased ================================================ FILE: .gitignore ================================================ prom-label-proxy ================================================ FILE: .golangci.yml ================================================ version: "2" linters: exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling rules: - linters: - errcheck path: _test.go paths: - third_party$ - builtin$ - examples$ formatters: exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: .promu.yml ================================================ --- go: # This must match .github/workflows/ci.yml. version: 1.26 repository: path: github.com/prometheus-community/prom-label-proxy build: binaries: - name: prom-label-proxy # yamllint disable rule:line-length ldflags: | -X github.com/prometheus/common/version.Version={{.Version}} -X github.com/prometheus/common/version.Revision={{.Revision}} -X github.com/prometheus/common/version.Branch={{.Branch}} -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} # yamllint enable rule:line-length tarball: files: - LICENSE crossbuild: platforms: - linux - darwin - windows - freebsd - openbsd - netbsd ================================================ FILE: .yamllint ================================================ --- extends: default ignore: | **/node_modules web/api/v1/testdata/openapi_*_golden.yaml rules: braces: max-spaces-inside: 1 level: error brackets: max-spaces-inside: 1 level: error commas: disable comments: disable comments-indentation: disable document-start: disable indentation: spaces: consistent indent-sequences: consistent key-duplicates: ignore: | config/testdata/section_key_dup.bad.yml line-length: disable truthy: check-keys: false ================================================ FILE: CHANGELOG.md ================================================ ## 0.13.0 / 2026-06-21 * [FEATURE] Add the `-insecure-skip-verify` flag to bypass the TLS verification of the upstream server. #335 * [FEATURE] Add the `-upstream-ca-cert` flag to provide the Certificate Authority's certificate of the upstream server. #340 * [FEATURE] Add the `-enable-promql-extended-range-selectors` flag to support extended range selectors in PromQL expressions. #358 * [FEATURE] Add the `-enable-promql-binop-fill-modifiers` flag to support binary operation fill modifiers in PromQL expressions. #358 ## 0.12.1 / 2025-09-11 * [BUGFIX] Don't panic on `error-on-replace` with multiple values. #300 ## 0.12.0 / 2025-08-06 * [ENHANCEMENT] Add the `-enable-promql-duration-expression-parsing` flag to support arithmetic for durations in PromQL expressions. #297 * [ENHANCEMENT] Add the `-enable-promql-experimental-functions` flag to support experimental functions in PromQL expressions. #297 * [ENHANCEMENT] Add the `-enable-label-matchers-for-rules-api` flag to filter rules using label matchers. #295 ## 0.11.1 / 2025-05-12 Rebuild with the latest Go compiler (`go1.24.3`). ## 0.11.0 / 2024-08-07 * [CHANGE] Return a 400 response code when the upstream response can't be modified. #228 * [CHANGE] Make `-error-on-replace` more cooperating. #233 * [FEATURE] Add the `-rules-with-active-alerts` flag to return rules with matching active alerts. #237 ## 0.10.0 / 2024-06-12 * [FEATURE] Add the `header-uses-list-syntax` flag to split the tenant header value on commas. #223 * [ENHANCEMENT] Support regex matcher for non-query Prometheus endpoints. #226 ## 0.9.0 / 2024-06-04 * [ENHANCEMENT] Update /api/v1/{rules,alerts} responses. #214 ## 0.8.1 / 2024-01-28 Internal change for library compatibility. No user-visible changes. * [CHANGE] Don't rely on slice labels #184 ## 0.8.0 / 2024-01-02 * [FEATURE] Add the `--regex-match` flag to filter with a regexp matcher. #171 ## 0.7.0 / 2023-06-15 * [FEATURE] Support filtering on multiple label values. #115 ## 0.6.0 / 2023-01-04 * [FEATURE] Add the `--header-name` flag to pass the label value via HTTP header. #118 * [FEATURE] Add the `--internal-listen-address` flag to expose Prometheus metrics. #121 * [FEATURE] Add the the `--label-value` flag to set the label value statically. #116 ## 0.5.0 / 2022-06-14 * [ENHANCEMENT] Add `/healthz` endpoint for (Kubernetes) probes. #106 ## 0.4.0 / 2021-10-05 * [ENHANCEMENT] Support HTTP POST for /api/v1/labels endpoint. #70 * [FEATURE] Add `--error-on-replace` flag (defaults to `false`) to return an error if a label value would otherwise be siltently replaced. #67 * [ENHANCEMENT] Add label enforce support for the new query_exemplars API. #65 ## 0.3.0 / 2021-04-16 * [FEATURE] Add support for /api/v1/series, /api/v1/labels and /api/v1/label//values endpoints (Prometheus/Thanos). #49 * [FEATURE] Add `-passthrough-paths` flag (empty by default), which allows exposing chosen resources from upstream without enforcing (e.g Prometheus UI). #48 * [ENHANCEMENT] Add support for queries via HTTP POST. #53 ## 0.2.0 / 2020-10-08 * [FEATURE] Add support for /api/v1/rules (Prometheus/Thanos). #16 * [FEATURE] Add support for /api/v1/alerts (Prometheus/Thanos). #18 * [FEATURE] Add support for /api/v2/silences (Alertmanager). #20 * [ENHANCEMENT] Enforce validity of the `-label` and `-upstream` CLI arguments. #33 * [ENHANCEMENT] Allow multiple enforcement matchers. #39 * [BUGFIX] Decompress gzipped response if needed. #35 ## 0.1.0 / 2018-10-24 Initial release. ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Prometheus Community Code of Conduct Prometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). ================================================ FILE: Dockerfile ================================================ ARG ARCH="amd64" ARG OS="linux" FROM quay.io/prometheus/busybox-${OS}-${ARCH}:glibc LABEL maintainer="The Prometheus Authors " ARG ARCH="amd64" ARG OS="linux" COPY .build/${OS}-${ARCH}/prom-label-proxy /bin/prom-label-proxy USER nobody ENTRYPOINT [ "/bin/prom-label-proxy" ] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MAINTAINERS.md ================================================ # Maintainers * Lucas Servén Marín (lserven@gmail.com / @squat) * Simon Pasquier (pasquier.simon@gmail.com / @simonpasquier) ================================================ FILE: Makefile ================================================ # Copyright 2020 The Prometheus Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Needs to be defined before including Makefile.common to auto-generate targets DOCKER_ARCHS ?= amd64 arm64 DOCKER_REPO ?= prometheuscommunity include Makefile.common STATICCHECK_IGNORE = DOCKER_IMAGE_NAME ?= prom-label-proxy .PHONY: run-curl-container run-curl-container: @echo 'Example: curl -v -s -k -H "Authorization: Bearer `cat /var/run/secrets/kubernetes.io/serviceaccount/token`" https://kube-rbac-proxy.default.svc:8443/api/v1/query?query=up\&namespace=default' kubectl run -i -t krp-curl --image=quay.io/brancz/krp-curl:v0.0.1 --restart=Never --command -- /bin/sh ================================================ FILE: Makefile.common ================================================ # Copyright The Prometheus Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # A common Makefile that includes rules to be reused in different prometheus projects. # !!! Open PRs only against the prometheus/prometheus/Makefile.common repository! # Example usage : # Create the main Makefile in the root project directory. # include Makefile.common # customTarget: # @echo ">> Running customTarget" # # Ensure GOBIN is not set during build so that promu is installed to the correct path unexport GOBIN GO ?= go GOFMT ?= $(GO)fmt FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) GOOPTS ?= GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH) GO_VERSION ?= $(shell $(GO) version) GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') PROMU := $(FIRST_GOPATH)/bin/promu pkgs = ./... ifeq (arm, $(GOHOSTARCH)) GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM) GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM) else GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH) endif GOTEST := $(GO) test GOTEST_DIR := ifneq ($(CIRCLE_JOB),) ifneq ($(shell command -v gotestsum 2> /dev/null),) GOTEST_DIR := test-results GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml -- endif endif PROMU_VERSION ?= 0.18.1 PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz SKIP_GOLANGCI_LINT := GOLANGCI_LINT := GOLANGCI_LINT_OPTS ?= GOLANGCI_LINT_VERSION ?= v2.11.4 GOLANGCI_FMT_OPTS ?= # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. # windows isn't included here because of the path separator being different. ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64)) # If we're in CI and there is an Actions file, that means the linter # is being run in Actions, so we don't need to run it here. ifneq (,$(SKIP_GOLANGCI_LINT)) GOLANGCI_LINT := else ifeq (,$(CIRCLE_JOB)) GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint else ifeq (,$(wildcard .github/workflows/golangci-lint.yml)) GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint endif endif endif PREFIX ?= $(shell pwd) BIN_DIR ?= $(shell pwd) DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) DOCKERBUILD_CONTEXT ?= ./ DOCKER_REPO ?= prom # Check if deprecated DOCKERFILE_PATH is set ifdef DOCKERFILE_PATH $(error DOCKERFILE_PATH is deprecated. Use DOCKERFILE_VARIANTS ?= $(DOCKERFILE_PATH) in the Makefile) endif DOCKER_ARCHS ?= amd64 arm64 armv7 ppc64le riscv64 s390x DOCKERFILE_VARIANTS ?= $(wildcard Dockerfile Dockerfile.*) # Function to extract variant from Dockerfile label. # Returns the variant name from io.prometheus.image.variant label, or "default" if not found. define dockerfile_variant $(strip $(or $(shell sed -n 's/.*io\.prometheus\.image\.variant="\([^"]*\)".*/\1/p' $(1)),default)) endef # Check for duplicate variant names (including default for Dockerfiles without labels). DOCKERFILE_VARIANT_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df))) DOCKERFILE_VARIANT_NAMES_SORTED := $(sort $(DOCKERFILE_VARIANT_NAMES)) ifneq ($(words $(DOCKERFILE_VARIANT_NAMES)),$(words $(DOCKERFILE_VARIANT_NAMES_SORTED))) $(error Duplicate variant names found. Each Dockerfile must have a unique io.prometheus.image.variant label, and only one can be without a label (default)) endif # Build variant:dockerfile pairs for shell iteration. DOCKERFILE_VARIANTS_WITH_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df)):$(df)) BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG)) ifeq ($(GOHOSTARCH),amd64) ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) # Only supported on amd64 test-flags := -race endif endif # This rule is used to forward a target like "build" to "common-build". This # allows a new "build" target to be defined in a Makefile which includes this # one and override "common-build" without override warnings. %: common-% ; .PHONY: common-all common-all: precheck style check_license lint yamllint unused build test .PHONY: common-style common-style: @echo ">> checking code style" @fmtRes=$$($(GOFMT) -d $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -name '*.go' -print)); \ if [ -n "$${fmtRes}" ]; then \ echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ echo "Please ensure you are using $$($(GO) version) for formatting code."; \ exit 1; \ fi .PHONY: common-check_license common-check_license: @echo ">> checking license header" @licRes=$$(for file in $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -type f -iname '*.go' -print) ; do \ awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ done); \ if [ -n "$${licRes}" ]; then \ echo "license header checking failed:"; echo "$${licRes}"; \ exit 1; \ fi @echo ">> checking for copyright years 2026 or later" @futureYearRes=$$(git grep -E 'Copyright (202[6-9]|20[3-9][0-9])' -- '*.go' ':!:vendor/*' || true); \ if [ -n "$${futureYearRes}" ]; then \ echo "Files with copyright year 2026 or later found (should use 'Copyright The Prometheus Authors'):"; echo "$${futureYearRes}"; \ exit 1; \ fi .PHONY: common-deps common-deps: @echo ">> getting dependencies" $(GO) mod download .PHONY: update-go-deps update-go-deps: @echo ">> updating Go dependencies" @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ $(GO) get $$m; \ done $(GO) mod tidy .PHONY: common-test-short common-test-short: $(GOTEST_DIR) @echo ">> running short tests" $(GOTEST) -short $(GOOPTS) $(pkgs) .PHONY: common-test common-test: $(GOTEST_DIR) @echo ">> running all tests" $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) $(GOTEST_DIR): @mkdir -p $@ .PHONY: common-format common-format: $(GOLANGCI_LINT) @echo ">> formatting code" $(GO) fmt $(pkgs) ifdef GOLANGCI_LINT @echo ">> formatting code with golangci-lint" $(GOLANGCI_LINT) fmt $(GOLANGCI_FMT_OPTS) endif .PHONY: common-vet common-vet: @echo ">> vetting code" $(GO) vet $(GOOPTS) $(pkgs) .PHONY: common-lint common-lint: $(GOLANGCI_LINT) ifdef GOLANGCI_LINT @echo ">> running golangci-lint" $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) endif .PHONY: common-lint-fix common-lint-fix: $(GOLANGCI_LINT) ifdef GOLANGCI_LINT @echo ">> running golangci-lint fix" $(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs) endif .PHONY: common-yamllint common-yamllint: @echo ">> running yamllint on all YAML files in the repository" ifeq (, $(shell command -v yamllint 2> /dev/null)) @echo "yamllint not installed so skipping" else yamllint . endif # For backward-compatibility. .PHONY: common-staticcheck common-staticcheck: lint .PHONY: common-unused common-unused: @echo ">> running check for unused/missing packages in go.mod" $(GO) mod tidy @git diff --exit-code -- go.sum go.mod .PHONY: common-build common-build: promu @echo ">> building binaries" $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) .PHONY: common-tarball common-tarball: promu @echo ">> building release tarball" $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) .PHONY: common-docker-repo-name common-docker-repo-name: @echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)" .PHONY: common-docker $(BUILD_DOCKER_ARCHS) common-docker: $(BUILD_DOCKER_ARCHS) $(BUILD_DOCKER_ARCHS): common-docker-%: @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ dockerfile=$${variant#*:}; \ variant_name=$${variant%%:*}; \ distroless_arch="$*"; \ if [ "$*" = "armv7" ]; then \ distroless_arch="arm"; \ fi; \ if [ "$$dockerfile" = "Dockerfile" ]; then \ echo "Building default variant ($$variant_name) for linux-$* using $$dockerfile"; \ docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ -f $$dockerfile \ --build-arg ARCH="$*" \ --build-arg OS="linux" \ --build-arg DISTROLESS_ARCH="$$distroless_arch" \ $(DOCKERBUILD_CONTEXT); \ if [ "$$variant_name" != "default" ]; then \ echo "Tagging default variant with $$variant_name suffix"; \ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ fi; \ else \ echo "Building $$variant_name variant for linux-$* using $$dockerfile"; \ docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" \ -f $$dockerfile \ --build-arg ARCH="$*" \ --build-arg OS="linux" \ --build-arg DISTROLESS_ARCH="$$distroless_arch" \ $(DOCKERBUILD_CONTEXT); \ fi; \ done .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) common-docker-publish: $(PUBLISH_DOCKER_ARCHS) $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ dockerfile=$${variant#*:}; \ variant_name=$${variant%%:*}; \ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ echo "Pushing $$variant_name variant for linux-$*"; \ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ fi; \ if [ "$$dockerfile" = "Dockerfile" ]; then \ echo "Pushing default variant ($$variant_name) for linux-$*"; \ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)"; \ fi; \ if [ "$(DOCKER_IMAGE_TAG)" = "latest" ]; then \ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ echo "Pushing $$variant_name variant version tags for linux-$*"; \ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \ fi; \ if [ "$$dockerfile" = "Dockerfile" ]; then \ echo "Pushing default variant version tag for linux-$*"; \ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"; \ fi; \ fi; \ done DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) common-docker-tag-latest: $(TAG_DOCKER_ARCHS) $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ dockerfile=$${variant#*:}; \ variant_name=$${variant%%:*}; \ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ echo "Tagging $$variant_name variant for linux-$* as latest"; \ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest-$$variant_name"; \ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \ fi; \ if [ "$$dockerfile" = "Dockerfile" ]; then \ echo "Tagging default variant ($$variant_name) for linux-$* as latest"; \ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest"; \ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"; \ fi; \ done .PHONY: common-docker-manifest common-docker-manifest: @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ dockerfile=$${variant#*:}; \ variant_name=$${variant%%:*}; \ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ echo "Creating manifest for $$variant_name variant"; \ refs=""; \ for arch in $(DOCKER_ARCHS); do \ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ done; \ if [ -z "$$refs" ]; then \ echo "Skipping manifest for $$variant_name variant (no supported architectures)"; \ continue; \ fi; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" $$refs; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ fi; \ if [ "$$dockerfile" = "Dockerfile" ]; then \ echo "Creating default variant ($$variant_name) manifest"; \ refs=""; \ for arch in $(DOCKER_ARCHS); do \ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)"; \ done; \ if [ -z "$$refs" ]; then \ echo "Skipping default variant manifest (no supported architectures)"; \ continue; \ fi; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $$refs; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)"; \ fi; \ if [ "$(DOCKER_IMAGE_TAG)" = "latest" ]; then \ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ echo "Creating manifest for $$variant_name variant version tag"; \ refs=""; \ for arch in $(DOCKER_ARCHS); do \ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \ done; \ if [ -z "$$refs" ]; then \ echo "Skipping version-tag manifest for $$variant_name variant (no supported architectures)"; \ continue; \ fi; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name" $$refs; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \ fi; \ if [ "$$dockerfile" = "Dockerfile" ]; then \ echo "Creating default variant version tag manifest"; \ refs=""; \ for arch in $(DOCKER_ARCHS); do \ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)"; \ done; \ if [ -z "$$refs" ]; then \ echo "Skipping default variant version-tag manifest (no supported architectures)"; \ continue; \ fi; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)" $$refs; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)"; \ fi; \ fi; \ done .PHONY: promu promu: $(PROMU) $(PROMU): $(eval PROMU_TMP := $(shell mktemp -d)) curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) mkdir -p $(FIRST_GOPATH)/bin cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu rm -r $(PROMU_TMP) .PHONY: common-proto common-proto: @echo ">> generating code from proto files" @./scripts/genproto.sh ifdef GOLANGCI_LINT $(GOLANGCI_LINT): mkdir -p $(FIRST_GOPATH)/bin curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ | sed -e '/install -d/d' \ | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) endif .PHONY: common-print-golangci-lint-version common-print-golangci-lint-version: @echo $(GOLANGCI_LINT_VERSION) .PHONY: precheck precheck:: define PRECHECK_COMMAND_template = precheck:: $(1)_precheck PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) .PHONY: $(1)_precheck $(1)_precheck: @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ exit 1; \ fi endef ================================================ FILE: OWNERS ================================================ component: "Monitoring" reviewers: - brancz - krasi-georgiev - metalmatze - paulfantom - pgier - s-urbaniak - simonpasquier - squat - lilic approvers: - brancz - krasi-georgiev - metalmatze - paulfantom - pgier - s-urbaniak - simonpasquier - squat - lilic ================================================ FILE: README.md ================================================ # prom-label-proxy [![Build Status](https://github.com/prometheus-community/prom-label-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/prometheus-community/prom-label-proxy/actions/workflows/ci.yml) [![Docker Repository on Quay](https://quay.io/repository/prometheuscommunity/prom-label-proxy/status "Docker Repository on Quay")](https://quay.io/repository/prometheuscommunity/prom-label-proxy) The prom-label-proxy can enforce a given label in a given PromQL query, in Prometheus API responses or in Alertmanager API requests. As an example (but not only), this allows read multi-tenancy for projects like Prometheus, Alertmanager or Thanos. This proxy does not perform authentication or authorization, this has to happen before the request reaches this proxy, allowing you to use any authN/authZ system you want. The [kube-rbac-proxy](https://github.com/brancz/kube-rbac-proxy) is an example for such an additional building block. Additionally, you can use prom-label-proxy as a library in your own proxy, like what is done in [prom-authzed-proxy](https://github.com/authzed/prom-authzed-proxy). ### Risks outside the scope of this project It's not a goal for this project to solve write tenant isolation for multi-tenant Prometheus: * If a tenant controls its scrape target configuration the tenant can set arbitrary labels via its relabelling configuration, thereby being able to pollute other tenant's metrics. * If the ingestion configuration [honor_labels](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) is set for a tenant's target, that target can pollute other tenant's metrics as Prometheus respects any labels exposed by the target. See [Prometheus Operator](https://github.com/prometheus-operator/prometheus-operator) label enforcement, [Thanos soft/hard tenancy](https://thanos.io/tip/proposals/201812_thanos-remote-receive.md/#architecture) or [Cortex](https://cortexmetrics.io/) as example solution to that. ## Installing `prom-label-proxy` ### Helm See: https://github.com/prometheus-community/helm-charts/tree/main/charts/prom-label-proxy ### Docker We publish docker images for each release, see: * [`quay.io/prometheuscommunity/prom-label-proxy`](https://quay.io/repository/prometheuscommunity/prom-label-proxy?tab=tags) for newest images * `quay.io/coreos/prom-label-proxy:v0.1.0` for the initial v0.1.0 release. ### Building from source If you want to build `prom-label-proxy` from source you would need a working installation of the [Go](https://golang.org/) 1.15+ [toolchain](https://github.com/golang/tools) (`GOPATH`, `PATH=${GOPATH}/bin:${PATH}`). `prom-label-proxy` can be downloaded and built by running: ```bash go get github.com/prometheus-community/prom-label-proxy ``` ## How does this project work? This application proxies the following endpoints and it ensures that a particular label is enforced in the particular request and response: * `/federate` for GET method (Prometheus) * `/api/v1/query_exemplars` for GET and POST methods (Prometheus/Thanos) * `/api/v1/query` for GET and POST methods (Prometheus/Thanos) * `/api/v1/query_range` for GET and POST methods (Prometheus/Thanos) * `/api/v1/series` for GET method (Prometheus/Thanos) * `/api/v1/rules` for GET method (Prometheus/Thanos) * `/api/v1/alerts` for GET method (Prometheus/Thanos) * `/api/v2/silences` for GET and POST methods (Alertmanager) * `/api/v2/silence/` for DELETE (Alertmanager) * `/api/v2/alerts/groups` for GET (Alertmanager) * `/api/v2/alerts` for GET (Alertmanager) When started with the `-enable-label-apis` flag, the application can also proxy the following endpoints: * `/api/v1/labels` for GET and POST methods (Prometheus/Thanos) * `/api/v1/label//values` for GET method (Prometheus/Thanos) You can run `prom-label-proxy` to enforce the value of the `tenant` label provided in the client's request via the `tenant` HTTP query/form parameter: ``` prom-label-proxy \ -query-param tenant \ -label tenant \ -upstream http://demo.do.prometheus.io:9090 \ -insecure-listen-address 127.0.0.1:8080 ``` Accessing the demo Prometheus APIs on `http://127.0.0.1:8080` will now expect that the client's request provides the `tenant` label value using the `tenant` HTTP query parameter: ```bash ➜ ~ curl http://127.0.0.1:8080/api/v1/query\?query="up" {"error":"The \"tenant\" query parameter must be provided.","errorType":"prom-label-proxy","status":"error"} ➜ ~ curl http://127.0.0.1:8080/api/v1/query\?query="up"\&tenant\="something" {"status":"success","data":{"resultType":"vector","result":[]}}% ``` You can provide multiple values for the label using several `tenant` HTTP query parameters: ```bash ➜ ~ curl http://127.0.0.1:8080/api/v1/query\?query="up"\&tenant\="something"\&tenant\="anything" {"status":"success","data":{"resultType":"vector","result":[]}}% ``` It also works with POST requests: ```bash ➜ ~ curl http://127.0.0.1:8080/api/v1/query" -d "tenant=foo" {"status":"success","data":{"resultType":"vector","result":[]}}% ``` Alternatively, `prom-label-proxy` can use a custom HTTP header instead HTTP parameters: ``` prom-label-proxy \ -header-name X-Tenant \ -label tenant \ -upstream http://demo.do.prometheus.io:9090 \ -insecure-listen-address 127.0.0.1:8080 ``` ```bash ➜ ~ curl -H 'X-Tenant: something' http://127.0.0.1:8080/api/v1/query\?query="up" {"status":"success","data":{"resultType":"vector","result":[]}}% ``` You can provide multiple values for the label using several HTTP headers: ```bash ➜ ~ curl -H 'X-Tenant=something' -H 'X-Tenant=anything' http://127.0.0.1:8080/api/v1/query\?query="up" {"status":"success","data":{"resultType":"vector","result":[]}}% ``` A last option is to provide a static value for the label: ``` prom-label-proxy \ -label tenant \ -label-value prometheus \ -upstream http://demo.do.prometheus.io:9090 \ -insecure-listen-address 127.0.0.1:8080 ``` Now prom-label-proxy enforces the `tenant="prometheus"` label in all requests. You can provide multiple static values for a label. For example: ``` prom-label-proxy \ -label tenant \ -label-value prometheus \ -label-value alertmanager \ -upstream http://demo.do.prometheus.io:9090 \ -insecure-listen-address 127.0.0.1:8080 ``` `prom-label-proxy` will enforce the `tenant=~"prometheus|alertmanager"` label selector in all requests. You can match the label value using a regular expression with the `-regex-match` option. For example: ``` prom-label-proxy \ -label-value '^foo-.+$' \ -label namespace \ -upstream http://demo.do.prometheus.io:9090 \ -insecure-listen-address 127.0.0.1:8080 \ -regex-match ``` > :warning: The above feature is experimental. Be careful when using this option, it may expose sensitive metrics if you use a too permissive expression. To error out when the query already contains a label matcher that conflicts with the one the proxy would inject, you can use the `-error-on-replace` option. For example: ``` prom-label-proxy \ -header-name X-Namespace \ -label namespace \ -upstream http://demo.do.prometheus.io:9090 \ -insecure-listen-address 127.0.0.1:8080 \ -error-on-replace ``` Once again for clarity: **this project only enforces a particular label in the respective calls to Prometheus, it in itself does not authenticate or authorize the requesting entity in any way, this has to be built around this project.** ### Federate endpoint The proxy ensures that all selectors passed as matchers to the `/federate` endpoint _must_ contain that exact match of the particular label (and throws away all other matchers for the label). ### Query endpoints For the two query endpoints (`/api/v1/query` and `/api/v1/query_range`), the proxy parses the PromQL expression and modifies all selectors in the same way. The label-key is configured as a flag on the binary and the label-value is passed as a query parameter. For example, if requesting the PromQL query ``` http_requests_total{namespace=~"a.*"} ``` and specifying the namespace label must be enforced to `b`, then the query will be re-written to ``` http_requests_total{namespace=~"b"} ``` This is enforced for any case, whether a label matcher is specified in the original query or not. ### Metadata endpoints Similar to query endpoint, for metadata endpoints `/api/v1/series`, `/api/v1/labels`, `/api/v1/label//values` the proxy injects the specified label all the provided `match[]` selectors. NOTE: When the `/api/v1/labels` and `/api/v1/label//values` endpoints were added to `prom-label-proxy`, the Prometheus and Thanos endpoints didn't support the `match[]` parameter hence the `prom-label-proxy` labels endpoints are disabled by default. Use the `-enable-label-apis` flag to enable with care. Ensure that the upstream endpoints support label selectors: * Prometheus >= [2.24.0](https://github.com/prometheus/prometheus/releases/tag/v2.24.0) * Thanos >= [v0.18.0](https://github.com/thanos-io/thanos/releases/tag/v0.18.0) at least, >= [0.23.0](https://github.com/thanos-io/thanos/releases/tag/v0.23.0) recommended for better performances. ### Rules endpoint The proxy requests the `/api/v1/rules` Prometheus endpoint, discards the rules that don't contain an exact match of the label(s) and returns the modified response to the client. To return alerting rules which have active alerts matching the label(s), you can use the `-rules-with-active-alerts` option. For example: ``` prom-label-proxy \ -header-name X-Namespace \ -label namespace \ -upstream http://demo.do.prometheus.io:9090 \ -insecure-listen-address 127.0.0.1:8080 \ -rules-with-active-alerts ``` If the upstream supports label matchers (Prometheus >= v2.54.0 and Thanos >= v0.25.0), you can use the `-enable-label-matchers-for-rules-api` option to filter the result at the source which is more efficient. ### Alerts endpoint The proxy requests the `/api/v1/alerts` Prometheus endpoint, discards the rules that don't contain an exact match of the label(s) and returns the modified response to the client. ### Silences endpoint The proxy ensures the following: * `GET` requests to the `/api/v2/silences` endpoint contain a `filter` parameter that matches exactly the particular label and throws away all other matchers for the label. * `POST` requests to the `/api/v2/silences` endpoint can only affect silences that match the label and the label matcher is enforced. * `DELETE` requests to the `/api/v2/silence/` endpoint can only affect silences that match the label. :rotating_light: `prom-label-proxy` doesn't support multiple label values for the Silences endpoints :rotating_light: ## Example use The concrete setup being shipped in OpenShift starting with 4.0: the proxy is configured to work with the label-key: namespace. In order to ensure that this is secure is it paired with the [kube-rbac-proxy](https://github.com/brancz/kube-rbac-proxy) and its URL rewrite functionality, meaning first ServiceAccount token authentication is performed, and then the kube-rbac-proxy authorization to see whether the requesting entity is allowed to retrieve the metrics for the requested namespace. The RBAC role we chose to authorize against is the same as the Kubernetes Resource Metrics API, the reasoning being, if an entity can `kubectl top pod` in a namespace, it can see cAdvisor metrics (container_memory_rss, container_cpu_usage_seconds_total, etc.). ================================================ FILE: SECURITY.md ================================================ # Reporting a security issue The Prometheus security policy, including how to report vulnerabilities, can be found here: [https://prometheus.io/docs/operating/security/](https://prometheus.io/docs/operating/security/) ================================================ FILE: VERSION ================================================ 0.13.0 ================================================ FILE: dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" ================================================ FILE: examples/caddy-port-based/Caddyfile ================================================ :8081 { rewrite * ?{query}&job=pushgateway reverse_proxy 127.0.0.1:8080 } :8082 { rewrite * ?{query}&job=prometheus reverse_proxy 127.0.0.1:8080 } ================================================ FILE: examples/caddy-port-based/README.md ================================================ # Demo: Using prom-label-proxy in front of demo Prometheus server. 1. Run prom-label-proxy with passthrough option: ``` prom-label-proxy -label job -upstream http://demo.robustperception.io:9090 -insecure-listen-address 127.0.0.1:8080 -non-api-path-passthrough ``` 2. In separate terminal run caddy that injects job=prometheus when accessing localhost:8082 and injecting job=pushgateway on localhost:8081: ``` docker run -it --rm --net=host -v $PWD/Caddyfile:/etc/caddy/Caddyfile caddy ``` 3. Access `localhost:8082` and compare with original `http://demo.robustperception.io:9090` server. You should be able to access only `job=pushgateway` or `job=prometheus` data depending on the port. ================================================ FILE: examples/kube-rbac-proxy/Dockerfile ================================================ FROM alpine RUN apk add --no-cache curl CMD /bin/sleep 3600 ================================================ FILE: examples/kube-rbac-proxy/client.yaml ================================================ --- apiVersion: apps/v1 kind: Deployment metadata: name: curl spec: selector: matchLabels: name: curl template: metadata: name: curl labels: name: curl spec: containers: - name: curl image: quay.io/brancz/curl:v0.0.1 ================================================ FILE: examples/kube-rbac-proxy/deployment.yaml ================================================ --- apiVersion: v1 kind: ServiceAccount metadata: name: kube-rbac-proxy --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: kube-rbac-proxy roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: kube-rbac-proxy subjects: - kind: ServiceAccount name: kube-rbac-proxy namespace: default --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: kube-rbac-proxy rules: - apiGroups: ["authentication.k8s.io"] resources: - tokenreviews verbs: ["create"] - apiGroups: ["authorization.k8s.io"] resources: - subjectaccessreviews verbs: ["create"] --- apiVersion: v1 kind: Service metadata: labels: app: kube-rbac-proxy name: kube-rbac-proxy spec: ports: - name: https port: 8443 targetPort: https selector: app: kube-rbac-proxy --- apiVersion: v1 kind: ConfigMap metadata: name: kube-rbac-proxy data: config.yaml: |+ authorization: rewrites: byQueryParameter: name: "namespace" resourceAttributes: apiVersion: v1beta1 apiGroup: metrics.k8s.io resource: pods namespace: "{{ .Value }}" --- apiVersion: extensions/v1beta1 kind: Deployment metadata: name: kube-rbac-proxy spec: replicas: 1 template: metadata: labels: app: kube-rbac-proxy spec: serviceAccountName: kube-rbac-proxy containers: - name: kube-rbac-proxy image: quay.io/brancz/kube-rbac-proxy:v0.4.0 args: - "--secure-listen-address=0.0.0.0:8443" - "--upstream=http://localhost:8080/" - "--config-file=/etc/kube-rbac-proxy/config.yaml" - "--logtostderr=true" - "--v=10" ports: - containerPort: 8443 name: https volumeMounts: - name: config mountPath: /etc/kube-rbac-proxy - name: prom-label-enforcer image: quay.io/coreos/prom-label-proxy:v0.1.0 imagePullPolicy: Always args: - "--insecure-listen-address=127.0.0.1:8080" - "--upstream=http://prometheus-k8s.monitoring.svc:9090/" - "--label=namespace" volumes: - name: config configMap: name: kube-rbac-proxy ================================================ FILE: examples/kube-rbac-proxy/rbac.yaml ================================================ --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: Role metadata: name: prom-label-proxy-client rules: - apiGroups: ["metrics.k8s.io/v1beta1"] resources: ["pods"] verbs: ["get"] --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: RoleBinding metadata: name: prom-label-proxy-client roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: prom-label-proxy-client subjects: - kind: ServiceAccount name: default namespace: default ================================================ FILE: go.mod ================================================ module github.com/prometheus-community/prom-label-proxy go 1.25.0 require ( github.com/efficientgo/core v1.0.0-rc.3 github.com/go-openapi/runtime v0.29.3 github.com/go-openapi/strfmt v0.26.1 github.com/metalmatze/signal v0.0.0-20210307161603-1c9aa721a97a github.com/oklog/run v1.2.0 github.com/prometheus/alertmanager v0.32.0 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/prometheus v0.311.2 gotest.tools/v3 v3.5.2 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.25.0 // indirect github.com/go-openapi/errors v0.22.7 // indirect github.com/go-openapi/jsonpointer v0.22.5 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect github.com/go-openapi/loads v0.23.3 // indirect github.com/go-openapi/spec v0.22.4 // indirect github.com/go-openapi/swag v0.25.5 // indirect github.com/go-openapi/swag/cmdutils v0.25.5 // indirect github.com/go-openapi/swag/conv v0.25.5 // indirect github.com/go-openapi/swag/fileutils v0.25.5 // indirect github.com/go-openapi/swag/jsonname v0.25.5 // indirect github.com/go-openapi/swag/jsonutils v0.25.5 // indirect github.com/go-openapi/swag/loading v0.25.5 // indirect github.com/go-openapi/swag/mangling v0.25.5 // indirect github.com/go-openapi/swag/netutils v0.25.5 // indirect github.com/go-openapi/swag/stringutils v0.25.5 // indirect github.com/go-openapi/swag/typeutils v0.25.5 // indirect github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/go-openapi/validate v0.25.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.16.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.42.0 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect go.opentelemetry.io/otel/trace v1.42.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM= github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/config v1.32.13 h1:5KgbxMaS2coSWRrx9TX/QtWbqzgQkOdEa3sZPhBhCSg= github.com/aws/aws-sdk-go-v2/config v1.32.13/go.mod h1:8zz7wedqtCbw5e9Mi2doEwDyEgHcEE9YOJp6a8jdSMY= github.com/aws/aws-sdk-go-v2/credentials v1.19.13 h1:mA59E3fokBvyEGHKFdnpNNrvaR351cqiHgRg+JzOSRI= github.com/aws/aws-sdk-go-v2/credentials v1.19.13/go.mod h1:yoTXOQKea18nrM69wGF9jBdG4WocSZA1h38A+t/MAsk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg= github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI= github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 h1:GcLE9ba5ehAQma6wlopUesYg/hbcOhFNWTjELkiWkh4= github.com/aws/aws-sdk-go-v2/service/sso v1.30.14/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 h1:mP49nTpfKtpXLt5SLn8Uv8z6W+03jYVoOSAl/c02nog= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w= github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U= github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/efficientgo/core v1.0.0-rc.3 h1:X6CdgycYWDcbYiJr1H1+lQGzx13o7bq3EUkbB9DsSPc= github.com/efficientgo/core v1.0.0-rc.3/go.mod h1:FfGdkzWarkuzOlY04VY+bGfb1lWrjaL6x/GLcQ4vJps= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs= github.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE= github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y= github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI= github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c= github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI= github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/metalmatze/signal v0.0.0-20210307161603-1c9aa721a97a h1:0usWxe5SGXKQovz3p+BiQ81Jy845xSMu2CWKuXsXuUM= github.com/metalmatze/signal v0.0.0-20210307161603-1c9aa721a97a/go.mod h1:3OETvrxfELvGsU2RoGGWercfeZ4bCL3+SOwzIWtJH/Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/alertmanager v0.32.0 h1:JER/KWXvbmSo6cd8EtnP7y+0VWKG8RiYH+hV/hHNYio= github.com/prometheus/alertmanager v0.32.0/go.mod h1:0Dy9faTtMgpVYxJVxV0o65elTxHnSRCF/7gy5BKGZiE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang/exp v0.0.0-20260325093428-d8591d0db856 h1:1Y6bmpZb8peQCy1IpctnAhIFuyhrdtMaDnETChhSNns= github.com/prometheus/client_golang/exp v0.0.0-20260325093428-d8591d0db856/go.mod h1:Vf0QcmVhGqpjLxZOaWrFSep86vchQtJmbztFaMM4f6Q= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/prometheus v0.311.2 h1:6fBxp93y08GAZGNT1o3bIhgV/AMYvBFfU+ltDNEsHg8= github.com/prometheus/prometheus v0.311.2/go.mod h1:gjsCxTKtHO1Q8T9333u1s+lUR1OjPyM7ruuGH8RvVyo= github.com/prometheus/sigv4 v0.4.1 h1:EIc3j+8NBea9u1iV6O5ZAN8uvPq2xOIUPcqCTivHuXs= github.com/prometheus/sigv4 v0.4.1/go.mod h1:eu+ZbRvsc5TPiHwqh77OWuCnWK73IdkETYY46P4dXOU= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= ================================================ FILE: injectproxy/alerts.go ================================================ // Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package injectproxy import "net/http" // alerts proxies HTTP requests to the Alertmanager /api/v2/alerts endpoint. func (r *routes) alerts(w http.ResponseWriter, req *http.Request) { switch req.Method { case "GET": r.enforceFilterParameter(w, req) default: http.NotFound(w, req) } } ================================================ FILE: injectproxy/alerts_test.go ================================================ // Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package injectproxy import ( "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) func TestGetAlerts(t *testing.T) { for _, tc := range []struct { labelv []string filters []string expCode int expQueryValues []string queryParam string url string }{ { // No "namespace" parameter returns an error. expCode: http.StatusBadRequest, url: "http://alertmanager.example.com/api/v2/alerts", }, { // Check that other query parameters are not removed. labelv: []string{"default"}, expCode: http.StatusOK, expQueryValues: []string{"false"}, queryParam: "silenced", url: "http://alertmanager.example.com/api/v2/alerts?silenced=false", }, { // Check that filter parameter is added when other query parameter are present. labelv: []string{"default"}, expCode: http.StatusOK, expQueryValues: []string{`namespace="default"`}, queryParam: "filter", url: "http://alertmanager.example.com/api/v2/alerts?silenced=false", }, { // Check that the filter parameter is added when multiple label values are set. labelv: []string{"default", "something"}, expCode: http.StatusOK, expQueryValues: []string{`namespace=~"default|something"`}, queryParam: "filter", url: "http://alertmanager.example.com/api/v2/alerts?silenced=false", }, { // Check that the original filter parameter is preserved when multiple label values are set. labelv: []string{"default", "something"}, filters: []string{`namespace="default"`, `instance=~".+"`}, expCode: http.StatusOK, expQueryValues: []string{`namespace=~"default|something"`, `namespace="default"`, `instance=~".+"`}, queryParam: "filter", url: "http://alertmanager.example.com/api/v2/alerts?silenced=false", }, { // Check that label values are correctly escaped. labelv: []string{"default", "some|thing"}, expCode: http.StatusOK, expQueryValues: []string{`namespace=~"default|some\\|thing"`}, queryParam: "filter", url: "http://alertmanager.example.com/api/v2/alerts?silenced=false", }, { // Check for filter parameter. labelv: []string{"default"}, filters: []string{`job="prometheus"`, `instance=~".+"`}, expCode: http.StatusOK, expQueryValues: []string{`job="prometheus"`, `instance=~".+"`, `namespace="default"`}, queryParam: "filter", url: "http://alertmanager.example.com/api/v2/alerts", }, } { t.Run(strings.Join(tc.filters, "&"), func(t *testing.T) { m := newMockUpstream(checkQueryHandler("", tc.queryParam, tc.expQueryValues...)) defer m.Close() r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}) if err != nil { t.Fatalf("unexpected error: %v", err) } u, err := url.Parse(tc.url) if err != nil { t.Fatalf("unexpected error: %v", err) } q := u.Query() for _, m := range tc.filters { q.Add("filter", m) } for _, lv := range tc.labelv { q.Add(proxyLabel, lv) } u.RawQuery = q.Encode() w := httptest.NewRecorder() req := httptest.NewRequest("GET", u.String(), nil) r.ServeHTTP(w, req) resp := w.Result() body, _ := io.ReadAll(resp.Body) defer resp.Body.Close() if resp.StatusCode != tc.expCode { t.Logf("expected status code %d, got %d", tc.expCode, resp.StatusCode) t.Logf("%s", string(body)) t.FailNow() } if resp.StatusCode != http.StatusOK { return } }) } } ================================================ FILE: injectproxy/enforce.go ================================================ // Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package injectproxy import ( "errors" "fmt" "slices" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql/parser" ) // PromQLEnforcer can enforce label matchers in PromQL expressions. type PromQLEnforcer struct { labelMatchers map[string]*labels.Matcher errorOnReplace bool parserOptions parser.Options } func NewPromQLEnforcer(errorOnReplace bool, ms ...*labels.Matcher) *PromQLEnforcer { return NewPromQLEnforcerWithOptions(errorOnReplace, defaultParserOptions(), ms...) } func NewPromQLEnforcerWithOptions(errorOnReplace bool, parserOptions parser.Options, ms ...*labels.Matcher) *PromQLEnforcer { entries := make(map[string]*labels.Matcher) for _, matcher := range ms { entries[matcher.Name] = matcher } return &PromQLEnforcer{ labelMatchers: entries, errorOnReplace: errorOnReplace, parserOptions: parserOptions, } } var ( // ErrQueryParse is returned when the input query is invalid. ErrQueryParse = errors.New("failed to parse query string") // ErrIllegalLabelMatcher is returned when the input query contains a conflicting label matcher. ErrIllegalLabelMatcher = errors.New("conflicting label matcher") // ErrEnforceLabel is returned when the label matchers couldn't be enforced. ErrEnforceLabel = errors.New("failed to enforce label") ) // Enforce the label matchers in a PromQL expression. func (ms *PromQLEnforcer) Enforce(q string) (string, error) { p := parser.NewParser(ms.parserOptions) expr, err := p.ParseExpr(q) if err != nil { return "", fmt.Errorf("%w: %w", ErrQueryParse, err) } if err := ms.EnforceNode(expr); err != nil { if errors.Is(err, ErrIllegalLabelMatcher) { return "", err } return "", fmt.Errorf("%w: %w", ErrEnforceLabel, err) } return expr.String(), nil } // EnforceNode walks the given node recursively // and enforces the given label enforcer on it. // // Whenever a parser.MatrixSelector or parser.VectorSelector AST node is found, // their label enforcer is being potentially modified. // If a node's label matcher has the same name as a label matcher // of the given enforcer, then it will be replaced. func (ms PromQLEnforcer) EnforceNode(node parser.Node) error { switch n := node.(type) { case *parser.EvalStmt: if err := ms.EnforceNode(n.Expr); err != nil { return err } case parser.Expressions: for _, e := range n { if err := ms.EnforceNode(e); err != nil { return err } } case *parser.AggregateExpr: if err := ms.EnforceNode(n.Expr); err != nil { return err } case *parser.BinaryExpr: if err := ms.EnforceNode(n.LHS); err != nil { return err } if err := ms.EnforceNode(n.RHS); err != nil { return err } case *parser.Call: if err := ms.EnforceNode(n.Args); err != nil { return err } case *parser.SubqueryExpr: if err := ms.EnforceNode(n.Expr); err != nil { return err } case *parser.ParenExpr: if err := ms.EnforceNode(n.Expr); err != nil { return err } case *parser.UnaryExpr: if err := ms.EnforceNode(n.Expr); err != nil { return err } case *parser.NumberLiteral, *parser.StringLiteral: // nothing to do case *parser.MatrixSelector: // inject labelselector if vs, ok := n.VectorSelector.(*parser.VectorSelector); ok { var err error vs.LabelMatchers, err = ms.EnforceMatchers(vs.LabelMatchers) if err != nil { return err } } case *parser.VectorSelector: // inject labelselector var err error n.LabelMatchers, err = ms.EnforceMatchers(n.LabelMatchers) if err != nil { return err } default: panic(fmt.Errorf("parser.Walk: unhandled node type %T", n)) } return nil } // EnforceMatchers appends the enforced label matcher(s) to the list of matchers // if not already present. // // If the label matcher that is to be injected is present (by labelname), the // behavior depends on the errorOnReplace variable and the enforced matcher(s): // * If errorOnReplace is false // - And the label matcher type is '=', the existing matcher is silently // discarded whatever is the original value. // - Otherwise the existing matcher is preserved. // // * if errorOnReplace is true // - And the label matcher and the enforced matcher are disjoint, the function returns an error. // - Otherwise the existing matcher is preserved. func (ms PromQLEnforcer) EnforceMatchers(targets []*labels.Matcher) ([]*labels.Matcher, error) { var res []*labels.Matcher for _, target := range targets { matcher, ok := ms.labelMatchers[target.Name] if !ok { res = append(res, target) continue } if ms.errorOnReplace { var ok bool // Ensure that the expression's matcher combined with the // enforced matchers can return some result. If the combined // matchers return no result, the function returns an error. // // For instance, when the enforced matcher is 'tenant="bar"': // * and the expression's selector is 'tenant="foo"' then the // result is always empty. // * and the expression's selector is 'tenant!="foo"' then the // matchers don't conflict. switch matcher.Type { case labels.MatchEqual: switch target.Type { case labels.MatchEqual: ok = matcher.Value == target.Value case labels.MatchNotEqual: ok = matcher.Value != target.Value case labels.MatchRegexp: ok = target.Matches(matcher.Value) case labels.MatchNotRegexp: ok = target.Matches(matcher.Value) } case labels.MatchNotEqual: switch target.Type { case labels.MatchEqual: ok = target.Value == "" || matcher.Matches(target.Value) case labels.MatchNotEqual: ok = true case labels.MatchRegexp: frm, _ := labels.NewFastRegexMatcher(target.Value) ok = (frm == nil || len(frm.SetMatches()) == 0) if !ok { for _, sm := range frm.SetMatches() { if sm != matcher.Value { ok = true break } } } case labels.MatchNotRegexp: ok = true } case labels.MatchRegexp: frm, _ := labels.NewFastRegexMatcher(matcher.Value) switch target.Type { case labels.MatchEqual: ok = matcher.Matches(target.Value) case labels.MatchNotEqual: if frm != nil { if slices.ContainsFunc(frm.SetMatches(), target.Matches) { ok = true } } ok = ok || (target.Value == "" && !matcher.Matches("")) case labels.MatchRegexp: if frm != nil { if slices.ContainsFunc(frm.SetMatches(), target.Matches) { ok = true } } case labels.MatchNotRegexp: if frm != nil { if slices.ContainsFunc(frm.SetMatches(), target.Matches) { ok = true } } ok = ok || (target.Value == "" && !matcher.Matches("")) } case labels.MatchNotRegexp: switch target.Type { case labels.MatchEqual: ok = target.Value != "" || !matcher.Matches("") case labels.MatchNotEqual: ok = true case labels.MatchRegexp: frm, _ := labels.NewFastRegexMatcher(target.Value) if frm != nil { if slices.ContainsFunc(frm.SetMatches(), matcher.Matches) { ok = true } } ok = ok && (target.Value != "" || !matcher.Matches("")) ok = ok && target.Value != matcher.Value case labels.MatchNotRegexp: ok = true } } if !ok { return res, fmt.Errorf("%w: label matcher %q conflicts with injected matcher %q", ErrIllegalLabelMatcher, target.String(), matcher.String()) } } // Always drop the expression matcher if: // * the enforced matcher is an equal matcher because it will be // added after iterating on all the expression's matchers. // * or it is equal to the enforced matcher. // In both cases, the enforced matcher will be added after // iterating on all the expression's matchers. if matcher.Type == labels.MatchEqual || matcher.String() == target.String() { continue } res = append(res, target) } for _, enforcedMatcher := range ms.labelMatchers { res = append(res, enforcedMatcher) } return res, nil } func defaultParserOptions() parser.Options { return parser.Options{} } ================================================ FILE: injectproxy/enforce_test.go ================================================ // Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package injectproxy import ( "errors" "fmt" "testing" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql/parser" ) func mustNewMatcher(t labels.MatchType, n, v string) *labels.Matcher { m, err := labels.NewMatcher(t, n, v) if err != nil { panic(err) } return m } type checkFunc func(expression string, err error) error func checks(cs ...checkFunc) checkFunc { return func(expression string, err error) error { for _, c := range cs { if e := c(expression, err); e != nil { return e } } return nil } } func noError() checkFunc { return func(_ string, got error) error { if got != nil { return fmt.Errorf("want error , got %v", got) } return nil } } func errorIs(want error) checkFunc { return func(_ string, got error) error { if errors.Is(got, want) { return nil } return fmt.Errorf("want error of type %T, got %v", want, got) } } func hasExpression(want string) checkFunc { return func(got string, _ error) error { if want != got { return fmt.Errorf("want expression \n%v\ngot \n%v", want, got) } return nil } } var tests = []struct { name string expression string enforcer *PromQLEnforcer check checkFunc }{ // first check correct label insertion { name: "expressions add label", expression: `round(metric1{label="baz"},3)`, enforcer: NewPromQLEnforcer( false, &labels.Matcher{ Name: "namespace", Type: labels.MatchEqual, Value: "NS", }, &labels.Matcher{ Name: "pod", Type: labels.MatchEqual, Value: "POD", }, ), check: checks( noError(), hasExpression(`round(metric1{label="baz",namespace="NS",pod="POD"}, 3)`), ), }, { name: "aggregate add label", expression: `sum by (pod) (metric1{label="baz"})`, enforcer: NewPromQLEnforcer( false, &labels.Matcher{ Name: "namespace", Type: labels.MatchEqual, Value: "NS", }, &labels.Matcher{ Name: "pod", Type: labels.MatchEqual, Value: "POD", }, ), check: checks( noError(), hasExpression(`sum by (pod) (metric1{label="baz",namespace="NS",pod="POD"})`), ), }, { name: "binary expression add label", expression: `metric1{} + sum by (pod) (metric2{label="baz"})`, enforcer: NewPromQLEnforcer( false, &labels.Matcher{ Name: "namespace", Type: labels.MatchEqual, Value: "NS", }, &labels.Matcher{ Name: "pod", Type: labels.MatchEqual, Value: "POD", }, ), check: checks( noError(), hasExpression(`metric1{namespace="NS",pod="POD"} + sum by (pod) (metric2{label="baz",namespace="NS",pod="POD"})`), ), }, { name: "binary expression with vector matching add label", expression: `metric1{} + on(pod,namespace) sum by (pod) (metric2{label="baz"})`, enforcer: NewPromQLEnforcer( false, &labels.Matcher{ Name: "namespace", Type: labels.MatchEqual, Value: "NS", }, &labels.Matcher{ Name: "pod", Type: labels.MatchEqual, Value: "POD", }, ), check: checks( noError(), hasExpression(`metric1{namespace="NS",pod="POD"} + on (pod, namespace) sum by (pod) (metric2{label="baz",namespace="NS",pod="POD"})`), ), }, // then check error return when a query would be silently altered, i.e. a label // matcher would be changed { name: "expressions error on non-matching label value", expression: `round(metric1{label="baz",pod="POD",namespace="bar"},3)`, enforcer: NewPromQLEnforcer( true, &labels.Matcher{ Name: "namespace", Type: labels.MatchEqual, Value: "NS", }, &labels.Matcher{ Name: "pod", Type: labels.MatchEqual, Value: "POD", }, ), check: checks( errorIs(ErrIllegalLabelMatcher), ), }, { name: "aggregate error on non-matching label value", expression: `sum by (pod) (metric1{label="baz",pod="foo",namespace="bar"})`, enforcer: NewPromQLEnforcer( true, &labels.Matcher{ Name: "namespace", Type: labels.MatchEqual, Value: "NS", }, &labels.Matcher{ Name: "pod", Type: labels.MatchEqual, Value: "POD", }, ), check: checks( errorIs(ErrIllegalLabelMatcher), ), }, { name: "binary expression error on non-matching label value", expression: `metric1{pod="baz"} + sum by (pod) (metric2{label="baz",pod="foo",namespace="bar"})`, enforcer: NewPromQLEnforcer( true, &labels.Matcher{ Name: "namespace", Type: labels.MatchEqual, Value: "NS", }, &labels.Matcher{ Name: "pod", Type: labels.MatchEqual, Value: "POD", }, ), check: checks( errorIs(ErrIllegalLabelMatcher), ), }, { name: "binary expression with vector matching error on non-matching label value", expression: `metric1{pod="baz"} + on (pod,namespace) sum by (pod) (metric2{label="baz",pod="foo",namespace="bar"})`, enforcer: NewPromQLEnforcer( true, &labels.Matcher{ Name: "namespace", Type: labels.MatchEqual, Value: "NS", }, &labels.Matcher{ Name: "pod", Type: labels.MatchEqual, Value: "POD", }, ), check: checks( errorIs(ErrIllegalLabelMatcher), ), }, // and lastly check that passing the label matcher we would inject // doesn't return an error { name: "expressions unchanged with matching label value", expression: `round(metric1{label="baz",pod="POD",namespace="NS"},3)`, enforcer: NewPromQLEnforcer( false, &labels.Matcher{ Name: "namespace", Type: labels.MatchEqual, Value: "NS", }, &labels.Matcher{ Name: "pod", Type: labels.MatchEqual, Value: "POD", }, ), check: checks( noError(), hasExpression(`round(metric1{label="baz",namespace="NS",pod="POD"}, 3)`), ), }, { name: "aggregate unchanged with matching label value", expression: `sum by (pod) (metric1{label="baz",pod="POD",namespace="NS"})`, enforcer: NewPromQLEnforcer( false, &labels.Matcher{ Name: "namespace", Type: labels.MatchEqual, Value: "NS", }, &labels.Matcher{ Name: "pod", Type: labels.MatchEqual, Value: "POD", }, ), check: checks( noError(), hasExpression(`sum by (pod) (metric1{label="baz",namespace="NS",pod="POD"})`), ), }, { name: "binary expression unchanged with matching label value", expression: `metric1{pod="POD"} + sum by (pod) (metric2{label="baz",namespace="NS",pod="POD"})`, enforcer: NewPromQLEnforcer( false, &labels.Matcher{ Name: "namespace", Type: labels.MatchEqual, Value: "NS", }, &labels.Matcher{ Name: "pod", Type: labels.MatchEqual, Value: "POD", }, ), check: checks( noError(), hasExpression(`metric1{namespace="NS",pod="POD"} + sum by (pod) (metric2{label="baz",namespace="NS",pod="POD"})`), ), }, { name: "binary expression with vector matching unchanged with matching label value", expression: `metric1{pod="POD"} + on (pod,namespace) sum by (pod) (metric2{label="baz",pod="POD",namespace="NS"})`, enforcer: NewPromQLEnforcer( false, &labels.Matcher{ Name: "namespace", Type: labels.MatchEqual, Value: "NS", }, &labels.Matcher{ Name: "pod", Type: labels.MatchEqual, Value: "POD", }, ), check: checks( noError(), hasExpression(`metric1{namespace="NS",pod="POD"} + on (pod, namespace) sum by (pod) (metric2{label="baz",namespace="NS",pod="POD"})`), ), }, { name: "invalid PromQL expression", expression: `metric1{pod="baz"`, enforcer: NewPromQLEnforcer( false, &labels.Matcher{ Name: "namespace", Type: labels.MatchEqual, Value: "NS", }, ), check: checks( errorIs(ErrQueryParse), ), }, } func TestEnforce(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got, err := tc.enforcer.Enforce(tc.expression) if err := tc.check(got, err); err != nil { t.Fatal(err) } }) } } func TestEnforceWithErrOnReplace(t *testing.T) { type subTestCase struct { labelSelector string exp string err bool } for _, tc := range []struct { enforcedMatcher *labels.Matcher stcs []subTestCase }{ // Equal matcher enforcer. { enforcedMatcher: mustNewMatcher(labels.MatchEqual, "job", "foo"), stcs: []subTestCase{ // No selector in the expression for the enforced label. { ``, `up{job="foo"}`, false, }, // Equal label selector in the expression. { `job=""`, ``, true, }, { `job="foo"`, `up{job="foo"}`, false, }, { `job="bar"`, ``, true, }, { `job="fred"`, ``, true, }, // Not equal label selector in the expression. { `job!=""`, `up{job="foo"}`, false, }, { `job!="foo"`, ``, true, }, { `job!="bar"`, `up{job="foo"}`, false, }, { `job!="fred"`, `up{job="foo"}`, false, }, // Regexp label selector in the expression. { `job=~""`, ``, true, }, { `job=~"foo"`, `up{job="foo"}`, false, }, { `job=~"bar"`, ``, true, }, { `job=~"fred"`, ``, true, }, { `job=~"foo|fred"`, `up{job="foo"}`, false, }, { `job=~"foo|bar"`, `up{job="foo"}`, false, }, // Not-regexp label selector in the expression. { `job!~""`, `up{job="foo"}`, false, }, { `job!~"foo"`, ``, true, }, { `job!~"bar"`, `up{job="foo"}`, false, }, { `job!~"fred"`, `up{job="foo"}`, false, }, { `job!~"foo|fred"`, ``, true, }, { `job!~"foo|bar"`, ``, true, }, }, }, // Not equal matcher enforcer. { enforcedMatcher: mustNewMatcher(labels.MatchNotEqual, "job", "foo"), stcs: []subTestCase{ // No selector in the expression for the enforced label. { ``, `up{job!="foo"}`, false, }, // Equal label selector in the expression. { `job=""`, `up{job!="foo",job=""}`, false, }, { `job="foo"`, ``, true, }, { `job="bar"`, `up{job!="foo",job="bar"}`, false, }, { `job="fred"`, `up{job!="foo",job="fred"}`, false, }, // Not equal label selector in the expression. { `job!=""`, `up{job!="",job!="foo"}`, false, }, { `job!="foo"`, `up{job!="foo"}`, false, }, { `job!="bar"`, `up{job!="bar",job!="foo"}`, false, }, { `job!="fred"`, `up{job!="foo",job!="fred"}`, false, }, // Regexp label selector in the expression. { `job=~""`, `up{job!="foo",job=~""}`, false, }, { // up{job!="foo",job=~"foo"} would return no result. `job=~"foo"`, ``, true, }, { `job=~"bar"`, `up{job!="foo",job=~"bar"}`, false, }, { `job=~"fred"`, `up{job!="foo",job=~"fred"}`, false, }, { `job=~"foo|fred"`, `up{job!="foo",job=~"foo|fred"}`, false, }, { `job=~"foo|bar"`, `up{job!="foo",job=~"foo|bar"}`, false, }, // Not-regexp label selector in the expression. { `job!~""`, `up{job!="foo",job!~""}`, false, }, { `job!~"foo"`, `up{job!="foo",job!~"foo"}`, false, }, { `job!~"bar"`, `up{job!="foo",job!~"bar"}`, false, }, { `job!~"fred"`, `up{job!="foo",job!~"fred"}`, false, }, { `job!~"foo|fred"`, `up{job!="foo",job!~"foo|fred"}`, false, }, { `job!~"foo|bar"`, `up{job!="foo",job!~"foo|bar"}`, false, }, }, }, // Regexp matcher enforcer. { enforcedMatcher: mustNewMatcher(labels.MatchRegexp, "job", "foo|bar"), stcs: []subTestCase{ // No selector in the expression for the enforced label. { ``, `up{job=~"foo|bar"}`, false, }, // Equal label selector in the expression. { `job=""`, ``, true, }, { `job="foo"`, `up{job="foo",job=~"foo|bar"}`, false, }, { `job="bar"`, `up{job="bar",job=~"foo|bar"}`, false, }, { `job="fred"`, ``, true, }, // Not equal label selector in the expression. { `job!=""`, `up{job!="",job=~"foo|bar"}`, false, }, { `job!="foo"`, `up{job!="foo",job=~"foo|bar"}`, false, }, { `job!="bar"`, `up{job!="bar",job=~"foo|bar"}`, false, }, { `job!="fred"`, `up{job!="fred",job=~"foo|bar"}`, false, }, // Regexp label selector in the expression. { `job=~""`, ``, true, }, { `job=~"foo"`, `up{job=~"foo",job=~"foo|bar"}`, false, }, { `job=~"bar"`, `up{job=~"bar",job=~"foo|bar"}`, false, }, { `job=~"fred"`, `up{job=~"foo|bar",job=~"fred"}`, true, }, { `job=~"foo|fred"`, `up{job=~"foo|bar",job=~"foo|fred"}`, false, }, { `job=~"foo|bar"`, `up{job=~"foo|bar"}`, false, }, // Not-regexp label selector in the expression. { `job!~""`, `up{job!~"",job=~"foo|bar"}`, false, }, { `job!~"foo"`, `up{job!~"foo",job=~"foo|bar"}`, false, }, { `job!~"bar"`, `up{job!~"bar",job=~"foo|bar"}`, false, }, { `job!~"fred"`, `up{job!~"fred",job=~"foo|bar"}`, false, }, { `job!~"foo|fred"`, `up{job!~"foo|fred",job=~"foo|bar"}`, false, }, { `job!~"foo|bar"`, ``, true, }, }, }, // Not regexp matcher enforcer. { enforcedMatcher: mustNewMatcher(labels.MatchNotRegexp, "job", "foo|bar"), stcs: []subTestCase{ // No selector in the expression for the enforced label. { ``, `up{job!~"foo|bar"}`, false, }, // Equal label selector in the expression. { `job=""`, ``, true, }, { `job="foo"`, `up{job!~"foo|bar",job="foo"}`, false, }, { `job="bar"`, `up{job!~"foo|bar",job="bar"}`, false, }, { `job="fred"`, `up{job!~"foo|bar",job="fred"}`, false, }, // Not equal label selector in the expression. { `job!=""`, `up{job!="",job!~"foo|bar"}`, false, }, { `job!="foo"`, `up{job!="foo",job!~"foo|bar"}`, false, }, { `job!="bar"`, `up{job!="bar",job!~"foo|bar"}`, false, }, { `job!="fred"`, `up{job!="fred",job!~"foo|bar"}`, false, }, // Regexp label selector in the expression. { `job=~""`, ``, true, }, { // up{job!~"foo|bar",job=~"foo"} would return no result. `job=~"foo"`, ``, true, }, { // up{job!~"foo|bar",job=~"bar"} would return no result. `job=~"bar"`, ``, true, }, { `job=~"fred"`, `up{job!~"foo|bar",job=~"fred"}`, false, }, { `job=~"foo|fred"`, `up{job!~"foo|bar",job=~"foo|fred"}`, false, }, { `job=~"foo|bar"`, ``, true, }, // Not-regexp label selector in the expression. { `job!~""`, `up{job!~"",job!~"foo|bar"}`, false, }, { `job!~"foo"`, `up{job!~"foo",job!~"foo|bar"}`, false, }, { `job!~"bar"`, `up{job!~"bar",job!~"foo|bar"}`, false, }, { `job!~"fred"`, `up{job!~"foo|bar",job!~"fred"}`, false, }, { `job!~"foo|fred"`, `up{job!~"foo|bar",job!~"foo|fred"}`, false, }, { `job!~"foo|bar"`, `up{job!~"foo|bar"}`, false, }, }, }, // More edge cases. { enforcedMatcher: mustNewMatcher(labels.MatchRegexp, "job", "foo|foo"), stcs: []subTestCase{ { `job!="foo"`, ``, true, }, { `job=""`, ``, true, }, { `job="foo"`, `up{job="foo",job=~"foo|foo"}`, false, }, { `job="foo",job="foo"`, `up{job="foo",job="foo",job=~"foo|foo"}`, false, }, { `job=~"foo"`, `up{job=~"foo",job=~"foo|foo"}`, false, }, { `job!~"foo"`, ``, true, }, { `job!~""`, `up{job!~"",job=~"foo|foo"}`, false, }, }, }, // Regexp matcher enforcer which doesn't compile to a list of strings. { enforcedMatcher: mustNewMatcher(labels.MatchRegexp, "job", "foo.*"), stcs: []subTestCase{ { // In theory, it should translate to // `up{job!="foo",job=~"foo.*"}` but the enforcer can't // understand (yet) that both matchers are compatible. `job!="foo"`, ``, true, }, { `job=""`, ``, true, }, { `job="foo"`, `up{job="foo",job=~"foo.*"}`, false, }, { `job="foo",job="foo"`, `up{job="foo",job="foo",job=~"foo.*"}`, false, }, { // In theory, it should translate to // `up{job=~"foo",job=~"foo.*"}` but the enforcer can't // understand (yet) that both matchers are compatible. `job=~"foo"`, ``, true, }, { // In theory, it should translate to // `up{job!~"foo",job=~"foo.*"}` but the enforcer can't // understand (yet) that both matchers are compatible. `job!~"foo"`, ``, true, }, { `job!~"foo.*"`, ``, true, }, { `job!~""`, `up{job!~"",job=~"foo.*"}`, false, }, }, }, } { t.Run(fmt.Sprintf("enforcer=%q", tc.enforcedMatcher.String()), func(t *testing.T) { enforcer := NewPromQLEnforcer(true, tc.enforcedMatcher) for _, stc := range tc.stcs { expr := fmt.Sprintf("up{%s}", stc.labelSelector) t.Run(expr, func(t *testing.T) { got, err := enforcer.Enforce(expr) if stc.err { if err == nil { t.Fatalf("expected error, got nil") } if !errors.Is(err, ErrIllegalLabelMatcher) { t.Fatalf("expected err,ErrIllegalLabelMatcher error, got %s", err) } return } if err != nil { t.Fatalf("expected no error, got %s", err.Error()) } if got != stc.exp { t.Fatalf("expected expression %q, got %q", stc.exp, got) } }) } }) } } func TestEnforceWithParserOptions(t *testing.T) { ns := mustNewMatcher(labels.MatchEqual, "namespace", "NS") for _, tc := range []struct { name string expression string enforcer *PromQLEnforcer check checkFunc }{ // EnableExperimentalFunctions { name: "experimental function enabled", expression: `mad_over_time(metric[5m])`, enforcer: NewPromQLEnforcerWithOptions(false, parser.Options{EnableExperimentalFunctions: true}, ns), check: checks( noError(), hasExpression(`mad_over_time(metric{namespace="NS"}[5m])`), ), }, { name: "experimental function disabled", expression: `mad_over_time(metric[5m])`, enforcer: NewPromQLEnforcerWithOptions(false, parser.Options{}, ns), check: errorIs(ErrQueryParse), }, // ExperimentalDurationExpr { name: "experimental duration expression enabled", expression: `rate(metric[5m * 2])`, enforcer: NewPromQLEnforcerWithOptions(false, parser.Options{ExperimentalDurationExpr: true}, ns), check: checks( noError(), hasExpression(`rate(metric{namespace="NS"}[5m * 2])`), ), }, { name: "experimental duration expression disabled", expression: `rate(metric[5m * 2])`, enforcer: NewPromQLEnforcerWithOptions(false, parser.Options{}, ns), check: errorIs(ErrQueryParse), }, // EnableExtendedRangeSelectors { name: "extended range selectors enabled", expression: `metric anchored`, enforcer: NewPromQLEnforcerWithOptions(false, parser.Options{EnableExtendedRangeSelectors: true}, ns), check: checks( noError(), hasExpression(`metric{namespace="NS"} anchored`), ), }, { name: "extended range selectors disabled", expression: `metric anchored`, enforcer: NewPromQLEnforcerWithOptions(false, parser.Options{}, ns), check: errorIs(ErrQueryParse), }, // EnableBinopFillModifiers { name: "binop fill modifiers enabled", expression: `metric_a + fill(0) metric_b`, enforcer: NewPromQLEnforcerWithOptions(false, parser.Options{EnableBinopFillModifiers: true}, ns), check: checks( noError(), hasExpression(`metric_a{namespace="NS"} + fill (0) metric_b{namespace="NS"}`), ), }, { name: "binop fill modifiers disabled", expression: `metric_a + fill(0) metric_b`, enforcer: NewPromQLEnforcerWithOptions(false, parser.Options{}, ns), check: errorIs(ErrQueryParse), }, } { t.Run(tc.name, func(t *testing.T) { got, err := tc.enforcer.Enforce(tc.expression) if err := tc.check(got, err); err != nil { t.Fatal(err) } }) } } ================================================ FILE: injectproxy/routes.go ================================================ // Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package injectproxy import ( "context" "crypto/tls" "crypto/x509" "encoding/json" "errors" "fmt" "io" "log" "net/http" "net/http/httputil" "net/url" "os" "regexp" "slices" "sort" "strings" "github.com/efficientgo/core/merrors" "github.com/metalmatze/signal/server/signalhttp" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql/parser" ) const ( queryParam = "query" matchersParam = "match[]" ) type routes struct { upstream *url.URL handler http.Handler label string el ExtractLabeler mux http.Handler modifiers map[string]func(*http.Response) error errorOnReplace bool regexMatch bool rulesWithActiveAlerts bool parserOpts parser.Options logger *log.Logger } type options struct { upstreamCaCert string enableLabelAPIs bool passthroughPaths []string insecureSkipVerify bool errorOnReplace bool registerer prometheus.Registerer regexMatch bool rulesWithActiveAlerts bool labelMatchersForRulesAPI bool parserOptions parser.Options } type Option interface { apply(*options) } type optionFunc func(*options) func (f optionFunc) apply(o *options) { f(o) } // WithPrometheusRegistry configures the proxy to use the given registerer. func WithPrometheusRegistry(reg prometheus.Registerer) Option { return optionFunc(func(o *options) { o.registerer = reg }) } // WithUpstreamCaCert configures the proxy to use the custom ca certificate for the upstream. func WithUpstreamCaCert(caCert string) Option { return optionFunc(func(o *options) { o.upstreamCaCert = caCert }) } // WithEnabledLabelsAPI enables proxying to labels API. If false, "501 Not implemented" will be return for those. func WithEnabledLabelsAPI() Option { return optionFunc(func(o *options) { o.enableLabelAPIs = true }) } // WithPassthroughPaths configures routes to register given paths as passthrough handlers for all HTTP methods. // that, if requested, will be forwarded without enforcing label. Use with care. // NOTE: Passthrough "all" paths like "/" or "" and regex are not allowed. func WithPassthroughPaths(paths []string) Option { return optionFunc(func(o *options) { o.passthroughPaths = paths }) } // insecureSkipVerify configures proxy to bypass validation of the server's TLS/SSL certificate. func WithInsecureSkipVerify() Option { return optionFunc(func(o *options) { o.insecureSkipVerify = true }) } // WithErrorOnReplace causes the proxy to return 400 if a label matcher we want to // inject is present in the query already and matches something different func WithErrorOnReplace() Option { return optionFunc(func(o *options) { o.errorOnReplace = true }) } // WithActiveAlerts causes the proxy to return rules with active alerts. func WithActiveAlerts() Option { return optionFunc(func(o *options) { o.rulesWithActiveAlerts = true }) } // WithLabelMatchersForRulesAPI instructs the proxy to use label matchers when querying the Rules API. func WithLabelMatchersForRulesAPI() Option { return optionFunc(func(o *options) { o.labelMatchersForRulesAPI = true }) } // WithRegexMatch causes the proxy to handle tenant name as regexp func WithRegexMatch() Option { return optionFunc(func(o *options) { o.regexMatch = true }) } // WithPromqlDurationExpressionParsing enables parsing of duration expressions in the PromQL parser. func WithPromqlDurationExpressionParsing() Option { return optionFunc(func(o *options) { o.parserOptions.ExperimentalDurationExpr = true }) } // WithPromqlExperimentalFunctions enables parsing of experimental functions in the PromQL parser. func WithPromqlExperimentalFunctions() Option { return optionFunc(func(o *options) { o.parserOptions.EnableExperimentalFunctions = true }) } // WithPromqlExtendedRangeSelectors enables extended range selectors in the PromQL parser. func WithPromqlExtendedRangeSelectors() Option { return optionFunc(func(o *options) { o.parserOptions.EnableExtendedRangeSelectors = true }) } // WithPromqlBinopFillModifiers enables binary operation fill modifiers in the PromQL parser. func WithPromqlBinopFillModifiers() Option { return optionFunc(func(o *options) { o.parserOptions.EnableBinopFillModifiers = true }) } // mux abstracts away the behavior we expect from the http.ServeMux type in this package. type mux interface { http.Handler Handle(string, http.Handler) } // strictMux is a mux that wraps standard HTTP handler with safer handler that allows safe user provided handler registrations. type strictMux struct { mux seen map[string]struct{} } func newStrictMux(m mux) *strictMux { return &strictMux{ m, map[string]struct{}{}, } } // Handle is like HTTP mux handle but it does not allow to register paths that are shared with previously registered paths. // It also makes sure the trailing / is registered too. // For example if /api/v1/federate was registered consequent registrations like /api/v1/federate/ or /api/v1/federate/some will // return error. In the mean time request with both /api/v1/federate and /api/v1/federate/ will point to the handled passed by /api/v1/federate // registration. // This allows to de-risk ability for user to mis-configure and leak inject isolation. func (s *strictMux) Handle(pattern string, handler http.Handler) error { sanitized := pattern for next := strings.TrimSuffix(sanitized, "/"); next != sanitized; sanitized = next { } if _, ok := s.seen[sanitized]; ok { return fmt.Errorf("pattern %q was already registered", sanitized) } for p := range s.seen { if strings.HasPrefix(sanitized+"/", p+"/") { return fmt.Errorf("pattern %q is registered, cannot register path %q that shares it", p, sanitized) } } s.mux.Handle(sanitized, handler) s.mux.Handle(sanitized+"/", handler) s.seen[sanitized] = struct{}{} return nil } // instrumentedMux wraps a mux and instruments it. type instrumentedMux struct { mux i signalhttp.HandlerInstrumenter } func newInstrumentedMux(m mux, r prometheus.Registerer) *instrumentedMux { return &instrumentedMux{ m, signalhttp.NewHandlerInstrumenter(r, []string{"handler"}), } } // Handle implements the mux interface. func (i *instrumentedMux) Handle(pattern string, handler http.Handler) { i.mux.Handle(pattern, i.i.NewHandler(prometheus.Labels{"handler": pattern}, handler)) } // ExtractLabeler is an HTTP handler that extract the label value to be // enforced from the HTTP request. If a valid label value is found, it should // store it in the request's context. Otherwise it should return an error in // the HTTP response (usually 400 or 500). type ExtractLabeler interface { ExtractLabel(next http.HandlerFunc) http.Handler } // HTTPFormEnforcer enforces a label value extracted from the HTTP form and query parameters. type HTTPFormEnforcer struct { ParameterName string } // ExtractLabel implements the ExtractLabeler interface. func (hff HTTPFormEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { labelValues, err := hff.getLabelValues(r) if err != nil { prometheusAPIError(w, humanFriendlyErrorMessage(err), http.StatusBadRequest) return } // Remove the proxy label from the query parameters. q := r.URL.Query() q.Del(hff.ParameterName) r.URL.RawQuery = q.Encode() // Remove the param from the PostForm. if r.Method == http.MethodPost { if err := r.ParseForm(); err != nil { prometheusAPIError(w, fmt.Sprintf("Failed to parse the PostForm: %v", err), http.StatusInternalServerError) return } if r.PostForm.Get(hff.ParameterName) != "" { r.PostForm.Del(hff.ParameterName) newBody := r.PostForm.Encode() // We are replacing request body, close previous one (r.FormValue ensures it is read fully and not nil). _ = r.Body.Close() r.Body = io.NopCloser(strings.NewReader(newBody)) r.ContentLength = int64(len(newBody)) } } next.ServeHTTP(w, r.WithContext(WithLabelValues(r.Context(), labelValues))) }) } func (hff HTTPFormEnforcer) getLabelValues(r *http.Request) ([]string, error) { err := r.ParseForm() if err != nil { return nil, fmt.Errorf("the form data can not be parsed: %w", err) } formValues := removeEmptyValues(r.Form[hff.ParameterName]) if len(formValues) == 0 { return nil, fmt.Errorf("the %q query parameter must be provided", hff.ParameterName) } return formValues, nil } // HTTPHeaderEnforcer enforces a label value extracted from the HTTP headers. type HTTPHeaderEnforcer struct { Name string ParseListSyntax bool } // ExtractLabel implements the ExtractLabeler interface. func (hhe HTTPHeaderEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { labelValues, err := hhe.getLabelValues(r) if err != nil { prometheusAPIError(w, humanFriendlyErrorMessage(err), http.StatusBadRequest) return } next.ServeHTTP(w, r.WithContext(WithLabelValues(r.Context(), labelValues))) }) } func (hhe HTTPHeaderEnforcer) getLabelValues(r *http.Request) ([]string, error) { headerValues := r.Header[hhe.Name] if hhe.ParseListSyntax { headerValues = trimValues(splitValues(headerValues, ",")) } headerValues = removeEmptyValues(headerValues) if len(headerValues) == 0 { return nil, fmt.Errorf("missing HTTP header %q", hhe.Name) } return headerValues, nil } // StaticLabelEnforcer enforces a static label value. type StaticLabelEnforcer []string // ExtractLabel implements the ExtractLabeler interface. func (sle StaticLabelEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next(w, r.WithContext(WithLabelValues(r.Context(), sle))) }) } func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, opts ...Option) (*routes, error) { opt := options{} for _, o := range opts { o.apply(&opt) } if opt.registerer == nil { opt.registerer = prometheus.NewRegistry() } proxy := httputil.NewSingleHostReverseProxy(upstream) r := &routes{ upstream: upstream, handler: proxy, label: label, el: extractLabeler, errorOnReplace: opt.errorOnReplace, regexMatch: opt.regexMatch, rulesWithActiveAlerts: opt.rulesWithActiveAlerts, logger: log.Default(), parserOpts: opt.parserOptions, } mux := newStrictMux(newInstrumentedMux(http.NewServeMux(), opt.registerer)) errs := merrors.New( mux.Handle("/federate", r.el.ExtractLabel(enforceMethods(r.matcher, "GET"))), mux.Handle("/api/v1/query", r.el.ExtractLabel(enforceMethods(r.query, "GET", "POST"))), mux.Handle("/api/v1/query_range", r.el.ExtractLabel(enforceMethods(r.query, "GET", "POST"))), mux.Handle("/api/v1/alerts", r.el.ExtractLabel(enforceMethods(r.passthrough, "GET"))), mux.Handle("/api/v1/series", r.el.ExtractLabel(enforceMethods(r.matcher, "GET", "POST"))), mux.Handle("/api/v1/query_exemplars", r.el.ExtractLabel(enforceMethods(r.query, "GET", "POST"))), ) if opt.labelMatchersForRulesAPI { errs.Add(mux.Handle("/api/v1/rules", r.el.ExtractLabel(enforceMethods(r.matcher, "GET")))) } else { errs.Add(mux.Handle("/api/v1/rules", r.el.ExtractLabel(enforceMethods(r.passthrough, "GET")))) } if opt.enableLabelAPIs { errs.Add( mux.Handle("/api/v1/labels", r.el.ExtractLabel(enforceMethods(r.matcher, "GET", "POST"))), // Full path is /api/v1/label//values but http mux does not support patterns. // This is fine though as we don't care about name for matcher injector. mux.Handle("/api/v1/label/", r.el.ExtractLabel(enforceMethods(r.matcher, "GET"))), ) } errs.Add( // Reject multi label values with assertSingleLabelValue() because the // semantics of the Silences API don't support multi-label matchers. mux.Handle("/api/v2/silences", r.el.ExtractLabel( r.errorIfRegexpMatch( enforceMethods( assertSingleLabelValue(r.silences), "GET", "POST", ), ), )), mux.Handle("/api/v2/silence/", r.el.ExtractLabel( r.errorIfRegexpMatch( enforceMethods( assertSingleLabelValue(r.deleteSilence), "DELETE", ), ), )), mux.Handle("/api/v2/alerts/groups", r.el.ExtractLabel(enforceMethods(r.enforceFilterParameter, "GET"))), mux.Handle("/api/v2/alerts", r.el.ExtractLabel(enforceMethods(r.alerts, "GET"))), ) errs.Add( mux.Handle("/healthz", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(map[string]bool{"ok": true}) })), ) if err := errs.Err(); err != nil { return nil, err } // Validate paths. for _, path := range opt.passthroughPaths { u, err := url.Parse(fmt.Sprintf("http://example.com%v", path)) if err != nil { return nil, fmt.Errorf("path %q is not a valid URI path, got %v", path, opt.passthroughPaths) } if u.Path != path { return nil, fmt.Errorf("path %q is not a valid URI path, got %v", path, opt.passthroughPaths) } if u.Path == "" || u.Path == "/" { return nil, fmt.Errorf("path %q is not allowed, got %v", u.Path, opt.passthroughPaths) } } // Register optional passthrough paths. for _, path := range opt.passthroughPaths { if err := mux.Handle(path, http.HandlerFunc(r.passthrough)); err != nil { return nil, err } } r.mux = mux r.modifiers = map[string]func(*http.Response) error{ "/api/v1/alerts": modifyAPIResponse(r.filterAlerts), } if !opt.labelMatchersForRulesAPI { r.modifiers["/api/v1/rules"] = modifyAPIResponse(r.filterRules) } // Configure tls for proxy transport := http.DefaultTransport.(*http.Transport).Clone() transport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: opt.insecureSkipVerify, } if opt.upstreamCaCert != "" { caCert, err := os.ReadFile(opt.upstreamCaCert) if err != nil { return nil, fmt.Errorf("failed to read CA certificate: %v", err) } caCertPool := x509.NewCertPool() if ok := caCertPool.AppendCertsFromPEM(caCert); !ok { return nil, fmt.Errorf("failed to append CA cert to pool") } transport.TLSClientConfig.RootCAs = caCertPool } proxy.Transport = transport proxy.ModifyResponse = r.ModifyResponse proxy.ErrorHandler = r.errorHandler proxy.ErrorLog = log.Default() return r, nil } func (r *routes) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.mux.ServeHTTP(w, req) } func (r *routes) ModifyResponse(resp *http.Response) error { m, found := r.modifiers[resp.Request.URL.Path] if !found { // Return the server's response unmodified. return nil } return m(resp) } func (r *routes) errorHandler(rw http.ResponseWriter, _ *http.Request, err error) { r.logger.Printf("http: proxy error: %v", err) if errors.Is(err, errModifyResponseFailed) { rw.WriteHeader(http.StatusBadRequest) } rw.WriteHeader(http.StatusBadGateway) } func enforceMethods(h http.HandlerFunc, methods ...string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { if slices.Contains(methods, req.Method) { h(w, req) return } http.NotFound(w, req) } } func (r *routes) errorIfRegexpMatch(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { if r.regexMatch { prometheusAPIError(w, "support for regex match not implemented", http.StatusNotImplemented) return } next(w, req) } } type ctxKey int const keyLabel ctxKey = iota // MustLabelValues returns labels (previously stored using WithLabelValue()) // from the given context. // It will panic if no label is found or the value is empty. func MustLabelValues(ctx context.Context) []string { labels, ok := ctx.Value(keyLabel).([]string) if !ok { panic(fmt.Sprintf("can't find the %q value in the context", keyLabel)) } if len(labels) == 0 { panic(fmt.Sprintf("empty %q value in the context", keyLabel)) } sort.Strings(labels) return labels } // MustLabelValue returns the first (alphabetical order) label value previously // stored using WithLabelValue() from the given context. // Similar to MustLabelValues, it will panic if no label is found or the value // is empty. func MustLabelValue(ctx context.Context) string { v := MustLabelValues(ctx) return v[0] } func labelValuesToRegexpString(labelValues []string) string { lvs := make([]string, len(labelValues)) for i := range labelValues { lvs[i] = regexp.QuoteMeta(labelValues[i]) } return strings.Join(lvs, "|") } // WithLabelValues stores labels in the given context. func WithLabelValues(ctx context.Context, labels []string) context.Context { return context.WithValue(ctx, keyLabel, labels) } func (r *routes) passthrough(w http.ResponseWriter, req *http.Request) { r.handler.ServeHTTP(w, req) } func (r *routes) query(w http.ResponseWriter, req *http.Request) { matcher, err := r.newLabelMatcher(MustLabelValues(req.Context())...) if err != nil { prometheusAPIError(w, err.Error(), http.StatusBadRequest) return } e := NewPromQLEnforcerWithOptions(r.errorOnReplace, r.parserOpts, matcher) // The `query` can come in the URL query string and/or the POST body. // For this reason, we need to try to enforcing in both places. // Note: a POST request may include some values in the URL query string // and others in the body. If both locations include a `query`, then // enforce in both places. q, found1, err := enforceQueryValues(e, req.URL.Query()) if err != nil { switch { case errors.Is(err, ErrIllegalLabelMatcher): prometheusAPIError(w, err.Error(), http.StatusBadRequest) case errors.Is(err, ErrQueryParse): prometheusAPIError(w, err.Error(), http.StatusBadRequest) case errors.Is(err, ErrEnforceLabel): prometheusAPIError(w, err.Error(), http.StatusInternalServerError) } return } req.URL.RawQuery = q var found2 bool // Enforce the query in the POST body if needed. if req.Method == http.MethodPost { if err := req.ParseForm(); err != nil { prometheusAPIError(w, err.Error(), http.StatusBadRequest) } q, found2, err = enforceQueryValues(e, req.PostForm) if err != nil { switch { case errors.Is(err, ErrIllegalLabelMatcher): prometheusAPIError(w, err.Error(), http.StatusBadRequest) case errors.Is(err, ErrQueryParse): prometheusAPIError(w, err.Error(), http.StatusBadRequest) case errors.Is(err, ErrEnforceLabel): prometheusAPIError(w, err.Error(), http.StatusInternalServerError) } return } // We are replacing request body, close previous one (ParseForm ensures it is read fully and not nil). _ = req.Body.Close() req.Body = io.NopCloser(strings.NewReader(q)) req.ContentLength = int64(len(q)) } // If no query was found, return early. if !found1 && !found2 { return } r.handler.ServeHTTP(w, req) } func enforceQueryValues(e *PromQLEnforcer, v url.Values) (values string, noQuery bool, err error) { // If no values were given or no query is present, // e.g. because the query came in the POST body // but the URL query string was passed, then finish early. if v.Get(queryParam) == "" { return v.Encode(), false, nil } q, err := e.Enforce(v.Get(queryParam)) if err != nil { return "", true, err } v.Set(queryParam, q) return v.Encode(), true, nil } func (r *routes) newLabelMatcher(vals ...string) (*labels.Matcher, error) { if r.regexMatch { if len(vals) != 1 { return nil, errors.New("only one label value allowed with regex match") } re := vals[0] compiledRegex, err := regexp.Compile(re) if err != nil { return nil, fmt.Errorf("invalid regex: %w", err) } if compiledRegex.MatchString("") { return nil, errors.New("regex should not match empty string") } return labels.NewMatcher(labels.MatchRegexp, r.label, re) } if len(vals) == 1 { return labels.NewMatcher( labels.MatchEqual, r.label, vals[0], ) } return labels.NewMatcher(labels.MatchRegexp, r.label, labelValuesToRegexpString(vals)) } // matcher modifies all the match[] HTTP parameters to match on the tenant label. // If none was provided, a tenant label matcher matcher is injected. // This works for non-query Prometheus API endpoints like /api/v1/series, // /api/v1/label//values, /api/v1/labels and /federate which support // multiple matchers. // See e.g https://prometheus.io/docs/prometheus/latest/querying/api/#querying-metadata func (r *routes) matcher(w http.ResponseWriter, req *http.Request) { matcher, err := r.newLabelMatcher(MustLabelValues(req.Context())...) if err != nil { prometheusAPIError(w, err.Error(), http.StatusBadRequest) return } q := req.URL.Query() if err := r.injectMatcher(q, matcher); err != nil { prometheusAPIError(w, err.Error(), http.StatusBadRequest) return } req.URL.RawQuery = q.Encode() if req.Method == http.MethodPost { if err := req.ParseForm(); err != nil { return } q = req.PostForm if err := r.injectMatcher(q, matcher); err != nil { return } // We are replacing request body, close previous one (ParseForm ensures it is read fully and not nil). _ = req.Body.Close() newBody := q.Encode() req.Body = io.NopCloser(strings.NewReader(newBody)) req.ContentLength = int64(len(newBody)) } r.handler.ServeHTTP(w, req) } func (r *routes) injectMatcher(q url.Values, matcher *labels.Matcher) error { matchers := q[matchersParam] if len(matchers) == 0 { q.Set(matchersParam, matchersToString(matcher)) return nil } // Inject label into existing matchers. p := parser.NewParser(r.parserOpts) for i, m := range matchers { ms, err := p.ParseMetricSelector(m) if err != nil { return err } matchers[i] = matchersToString(append(ms, matcher)...) } q[matchersParam] = matchers return nil } func matchersToString(ms ...*labels.Matcher) string { var el []string for _, m := range ms { el = append(el, m.String()) } return fmt.Sprintf("{%v}", strings.Join(el, ",")) } // humanFriendlyErrorMessage returns an error message with a capitalized first letter // and a punctuation at the end. func humanFriendlyErrorMessage(err error) string { if err == nil { return "" } errMsg := err.Error() return fmt.Sprintf("%s%s.", strings.ToUpper(errMsg[:1]), errMsg[1:]) } func splitValues(slice []string, sep string) []string { for i := 0; i < len(slice); { splitResult := strings.Split(slice[i], sep) slice = append(slice[:i], append(splitResult, slice[i+1:]...)...) i += len(splitResult) } return slice } func removeEmptyValues(slice []string) []string { for i := 0; i < len(slice); i++ { if slice[i] == "" { slice = append(slice[:i], slice[i+1:]...) i-- } } return slice } func trimValues(slice []string) []string { for i := range slice { slice[i] = strings.TrimSpace(slice[i]) } return slice } ================================================ FILE: injectproxy/routes_test.go ================================================ // Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package injectproxy import ( "fmt" "io" "net/http" "net/http/httptest" "net/url" "sort" "strings" "testing" "time" ) var okResponse = []byte(`ok`) func checkParameterAbsent(param string, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { kvs, err := url.ParseQuery(req.URL.RawQuery) if err != nil { prometheusAPIError(w, fmt.Sprintf("unexpected error: %v", err), http.StatusInternalServerError) return } if len(kvs[param]) != 0 { prometheusAPIError(w, fmt.Sprintf("unexpected parameter %q", param), http.StatusInternalServerError) return } next.ServeHTTP(w, req) }) } func checkFormParameterAbsent(param string, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { err := req.ParseForm() if err != nil { prometheusAPIError(w, fmt.Sprintf("unexpected error: %v", err), http.StatusInternalServerError) return } kvs := req.Form if len(kvs[param]) != 0 { prometheusAPIError(w, fmt.Sprintf("unexpected Form parameter %q", param), http.StatusInternalServerError) return } next.ServeHTTP(w, req) }) } // checkQueryHandler verifies that the request form contains the given parameter key/values. func checkQueryHandler(body, key string, values ...string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { kvs, err := url.ParseQuery(req.URL.RawQuery) if err != nil { prometheusAPIError(w, fmt.Sprintf("unexpected error: %v", err), http.StatusInternalServerError) return } // Verify that the client provides the parameter only once. if len(kvs[key]) != len(values) { prometheusAPIError(w, fmt.Sprintf("expected %d values of parameter %q, got %d", len(values), key, len(kvs[key])), http.StatusInternalServerError) return } sort.Strings(values) sort.Strings(kvs[key]) for i := range values { if kvs[key][i] != values[i] { prometheusAPIError(w, fmt.Sprintf("expected parameter %q with value %q, got %q", key, values[i], kvs[key][i]), http.StatusInternalServerError) return } } buf, err := io.ReadAll(req.Body) if err != nil { prometheusAPIError(w, "failed to read body", http.StatusInternalServerError) return } if string(buf) != body { prometheusAPIError(w, fmt.Sprintf("expected body %q, got %q", body, string(buf)), http.StatusInternalServerError) return } w.Write(okResponse) <-time.After(100) }) } // checkFormHandler verifies that the request Form contains the given parameter key/values. func checkFormHandler(key string, values ...string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { err := req.ParseForm() if err != nil { prometheusAPIError(w, fmt.Sprintf("unexpected error: %v", err), http.StatusInternalServerError) return } kvs := req.PostForm // Verify that the client provides the parameter only once. if len(kvs[key]) != len(values) { prometheusAPIError(w, fmt.Sprintf("expected %d values of parameter %q, got %d", len(values), key, len(kvs[key])), http.StatusInternalServerError) return } sort.Strings(values) sort.Strings(kvs[key]) for i := range values { if kvs[key][i] != values[i] { prometheusAPIError(w, fmt.Sprintf("expected parameter %q with value %q, got %q", key, values[i], kvs[key][i]), http.StatusInternalServerError) return } } w.Write(okResponse) <-time.After(100) }) } // mockUpstream simulates an upstream HTTP server. It runs on localhost. type mockUpstream struct { h http.Handler srv *httptest.Server url *url.URL } func newMockUpstream(h http.Handler) *mockUpstream { m := mockUpstream{h: h} m.srv = httptest.NewServer(&m) u, err := url.Parse(m.srv.URL) if err != nil { panic(err) } m.url = u return &m } func (m *mockUpstream) ServeHTTP(w http.ResponseWriter, req *http.Request) { m.h.ServeHTTP(w, req) } func (m *mockUpstream) Close() { m.srv.Close() } const proxyLabel = "namespace" func TestWithPassthroughPaths(t *testing.T) { m := newMockUpstream(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write(okResponse) })) defer m.Close() t.Run("invalid passthrough options", func(t *testing.T) { // Duplicated /api. _, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{"/api1", "/api2/something", "/api1"})) if err == nil { t.Fatal("expected error") } // Wrong format, params in path. _, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{"/api1?args=1", "/api1"})) if err == nil { t.Fatal("expected error") } // / is not allowed. _, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{"/", "/api2/something", "/api1"})) if err == nil { t.Fatal("expected error") } // "" is not allowed. _, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{"/api1", "/api2/something", "", "/api3"})) if err == nil { t.Fatal("expected error") } // Duplication with existing enforced path is not allowed. _, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{"/api1", "/api2/something", "/federate", "/api3"})) if err == nil { t.Fatal("expected error") } // Duplication with existing enforced path is not allowed. _, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{"/api1", "/api2/something", "/federate/", "/api3"})) if err == nil { t.Fatal("expected error") } // Duplication with existing enforced path is not allowed. _, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{"/api1", "/api2/something", "/federate/some", "/api3"})) if err == nil { t.Fatal("expected error") } // api4 is not valid URL path (does not start with /) _, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{"/api1", "/api2/something", "api4", "/api3"})) if err == nil { t.Fatal("expected error") } // api4/ is not valid URL path (does not start with /) _, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{"/api1", "/api2/something", "api4/", "/api3"})) if err == nil { t.Fatal("expected error") } // api4/something is not valid URL path (does not start with /) _, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{"/api1", "/api2/something", "api4/something", "/api3"})) if err == nil { t.Fatal("expected error") } }) r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{"/api1", "/api2/something", "/graph/"})) if err != nil { t.Fatalf("unexpected error: %v", err) } for _, tcase := range []struct { url string method string expCode int }{ { url: "http://prometheus.example.com/graph?namespace=ns1", method: http.MethodGet, expCode: http.StatusOK, }, { url: "http://prometheus.example.com/graph", method: http.MethodPost, expCode: http.StatusOK, }, { url: "http://prometheus.example.com/graph2", method: http.MethodPost, expCode: http.StatusNotFound, }, { url: "http://prometheus.example.com/api/v2/silence", method: http.MethodGet, expCode: http.StatusBadRequest, // Missing label to inject. }, { url: "http://prometheus.example.com/api1?yolo=ns1", method: http.MethodGet, expCode: http.StatusOK, }, { url: "http://prometheus.example.com/api2/something", method: http.MethodGet, expCode: http.StatusOK, }, } { t.Run(tcase.url, func(t *testing.T) { w := httptest.NewRecorder() r.ServeHTTP(w, httptest.NewRequest(tcase.method, tcase.url, nil)) resp := w.Result() if resp.StatusCode != tcase.expCode { b, err := io.ReadAll(resp.Body) fmt.Println(string(b), err) t.Fatalf("expected status code %v, got %d", tcase.expCode, resp.StatusCode) } }) } } func TestMatch(t *testing.T) { for _, tc := range []struct { labelv []string matches []string opts []Option expCode int expMatch []string expBody []byte }{ { // No "namespace" parameter returns an error. expCode: http.StatusBadRequest, }, { // No "match" parameter. labelv: []string{"default"}, expCode: http.StatusOK, expMatch: []string{`{namespace="default"}`}, expBody: okResponse, }, { // Single "match" parameters. labelv: []string{"default"}, matches: []string{`{job="prometheus",__name__=~"job:.*"}`}, expCode: http.StatusOK, expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`}, expBody: okResponse, }, { // Single "match" parameters with multiple label values. labelv: []string{"default", "something"}, matches: []string{`{job="prometheus",__name__=~"job:.*"}`}, expCode: http.StatusOK, expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace=~"default|something"}`}, expBody: okResponse, }, { // Check that label values are correctly escaped. labelv: []string{"default", "some|thing"}, matches: []string{`{job="prometheus",__name__=~"job:.*"}`}, expCode: http.StatusOK, expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace=~"default|some\\|thing"}`}, expBody: okResponse, }, { // Single "match" parameters with label dup name. labelv: []string{"default"}, matches: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`}, expCode: http.StatusOK, expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default",namespace="default"}`}, expBody: okResponse, }, { // Many "match" parameters. labelv: []string{"default"}, matches: []string{`{job="prometheus"}`, `{__name__=~"job:.*"}`}, expCode: http.StatusOK, expMatch: []string{`{job="prometheus",namespace="default"}`, `{__name__=~"job:.*",namespace="default"}`}, expBody: okResponse, }, { // Many "match" parameters with multiple label values. labelv: []string{"default", "something"}, matches: []string{ `{job="prometheus"}`, `{__name__=~"job:.*"}`, `{namespace="something"}`, }, expCode: http.StatusOK, expMatch: []string{ `{job="prometheus",namespace=~"default|something"}`, `{__name__=~"job:.*",namespace=~"default|something"}`, `{namespace="something",namespace=~"default|something"}`, }, expBody: okResponse, }, { // Many "match" parameters with a single regex value. labelv: []string{".+-monitoring"}, matches: []string{ `{job="prometheus"}`, `{__name__=~"job:.*"}`, `{namespace="something"}`, }, opts: []Option{WithRegexMatch()}, expCode: http.StatusOK, expMatch: []string{ `{job="prometheus",namespace=~".+-monitoring"}`, `{__name__=~"job:.*",namespace=~".+-monitoring"}`, `{namespace="something",namespace=~".+-monitoring"}`, }, expBody: okResponse, }, { // A single "match" parameter with multiple regex values. labelv: []string{"default", "something"}, matches: []string{ `{job="prometheus"}`, }, opts: []Option{WithRegexMatch()}, expCode: http.StatusBadRequest, }, { // A single "match" parameter with a regex value matching the empty string. labelv: []string{".*"}, matches: []string{ `{job="prometheus"}`, }, opts: []Option{WithRegexMatch()}, expCode: http.StatusBadRequest, }, } { for _, u := range []string{ "http://prometheus.example.com/federate", "http://prometheus.example.com/api/v1/labels", "http://prometheus.example.com/api/v1/label/some_label/values", } { t.Run(fmt.Sprintf("%s?match[]=%s", u, strings.Join(tc.matches, "&")), func(t *testing.T) { m := newMockUpstream( checkParameterAbsent( proxyLabel, checkQueryHandler("", matchersParam, tc.expMatch...), ), ) defer m.Close() r, err := NewRoutes( m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, append([]Option{WithEnabledLabelsAPI()}, tc.opts...)..., ) if err != nil { t.Fatalf("unexpected error: %v", err) } u, err := url.Parse(u) if err != nil { t.Fatalf("unexpected error: %v", err) } q := u.Query() for _, m := range tc.matches { q.Add(matchersParam, m) } for _, lv := range tc.labelv { q.Add(proxyLabel, lv) } u.RawQuery = q.Encode() w := httptest.NewRecorder() req := httptest.NewRequest("GET", u.String(), nil) r.ServeHTTP(w, req) resp := w.Result() body, _ := io.ReadAll(resp.Body) defer resp.Body.Close() if resp.StatusCode != tc.expCode { t.Logf("expected status code %d, got %d", tc.expCode, resp.StatusCode) t.Logf("%s", string(body)) t.FailNow() } if resp.StatusCode != http.StatusOK { return } if string(body) != string(tc.expBody) { t.Fatalf("expected body %q, got %q", string(tc.expBody), string(body)) } }) } } } func TestMatchWithPost(t *testing.T) { for _, tc := range []struct { labelv []string matches []string expCode int expMatch []string expBody []byte }{ { // No "namespace" parameter returns an error. expCode: http.StatusBadRequest, }, { // No "match" parameter. labelv: []string{"default"}, expCode: http.StatusOK, expMatch: []string{`{namespace="default"}`}, expBody: okResponse, }, { // Single "match" parameters. labelv: []string{"default"}, matches: []string{`{job="prometheus",__name__=~"job:.*"}`}, expCode: http.StatusOK, expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`}, expBody: okResponse, }, { // Single "match" parameters with multiple label values. labelv: []string{"default", "something"}, matches: []string{`{job="prometheus",__name__=~"job:.*"}`}, expCode: http.StatusOK, expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace=~"default|something"}`}, expBody: okResponse, }, { // Check that label values are correctly escaped. labelv: []string{"default", "some|thing"}, matches: []string{`{job="prometheus",__name__=~"job:.*"}`}, expCode: http.StatusOK, expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace=~"default|some\\|thing"}`}, expBody: okResponse, }, { // Single "match" parameters with label dup name. labelv: []string{"default"}, matches: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`}, expCode: http.StatusOK, expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default",namespace="default"}`}, expBody: okResponse, }, { // Many "match" parameters. labelv: []string{"default"}, matches: []string{`{job="prometheus"}`, `{__name__=~"job:.*"}`}, expCode: http.StatusOK, expMatch: []string{`{job="prometheus",namespace="default"}`, `{__name__=~"job:.*",namespace="default"}`}, expBody: okResponse, }, { // Many "match" parameters with multiple label values. labelv: []string{"default", "something"}, matches: []string{ `{job="prometheus"}`, `{__name__=~"job:.*"}`, `{namespace="something"}`, }, expCode: http.StatusOK, expMatch: []string{ `{job="prometheus",namespace=~"default|something"}`, `{__name__=~"job:.*",namespace=~"default|something"}`, `{namespace="something",namespace=~"default|something"}`, }, expBody: okResponse, }, } { for _, u := range []string{ "http://prometheus.example.com/api/v1/labels", } { t.Run(fmt.Sprintf("%s?match[]=%s", u, strings.Join(tc.matches, "&")), func(t *testing.T) { m := newMockUpstream( checkFormParameterAbsent( proxyLabel, checkFormHandler(matchersParam, tc.expMatch...), ), ) defer m.Close() r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithEnabledLabelsAPI()) if err != nil { t.Fatalf("unexpected error: %v", err) } u, err := url.Parse(u) if err != nil { t.Fatalf("unexpected error: %v", err) } q := url.Values{} for _, m := range tc.matches { q.Add(matchersParam, m) } for _, lv := range tc.labelv { q.Add(proxyLabel, lv) } w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, u.String(), strings.NewReader(q.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.ServeHTTP(w, req) resp := w.Result() body, _ := io.ReadAll(resp.Body) defer resp.Body.Close() if resp.StatusCode != tc.expCode { t.Logf("expected status code %d, got %d", tc.expCode, resp.StatusCode) t.Logf("%s", string(body)) t.FailNow() } if resp.StatusCode != http.StatusOK { return } if string(body) != string(tc.expBody) { t.Fatalf("expected body %q, got %q", string(tc.expBody), string(body)) } }) } } } func TestSeries(t *testing.T) { for _, tc := range []struct { name string labelv []string promQuery string expResponse []byte expCode int expMatch []string expBody []byte }{ { name: `No "namespace" parameter returns an error`, expCode: http.StatusBadRequest, }, { name: `No "namespace" parameter returns an error for POSTs`, expCode: http.StatusBadRequest, }, { name: `No "match[]" parameter returns 200 with empty body`, labelv: []string{"default"}, expMatch: []string{`{namespace="default"}`}, expResponse: okResponse, expCode: http.StatusOK, }, { name: `No "match[]" parameter returns 200 with empty body for POSTs`, labelv: []string{"default"}, expMatch: []string{`{namespace="default"}`}, expResponse: okResponse, expCode: http.StatusOK, }, { name: `Series`, labelv: []string{"default"}, promQuery: "up", expCode: http.StatusOK, expMatch: []string{`{__name__="up",namespace="default"}`}, expResponse: okResponse, }, { name: `Series with multiple label values`, labelv: []string{"default", "something"}, promQuery: "up", expCode: http.StatusOK, expMatch: []string{`{__name__="up",namespace=~"default|something"}`}, expResponse: okResponse, }, { name: `Series: check that label values are correctly escaped`, labelv: []string{"default", "some|thing"}, promQuery: "up", expCode: http.StatusOK, expMatch: []string{`{__name__="up",namespace=~"default|some\\|thing"}`}, expResponse: okResponse, }, { name: `Series with multiple labels`, labelv: []string{"default"}, promQuery: `up{instance="localhost:9090"}`, expCode: http.StatusOK, expMatch: []string{`{instance="localhost:9090",__name__="up",namespace="default"}`}, expResponse: okResponse, }, { name: `Series with multiple label values and existing matcher`, labelv: []string{"default", "something"}, promQuery: `up{instance="localhost:9090",namespace="something"}`, expCode: http.StatusOK, expMatch: []string{`{instance="localhost:9090",namespace="something",__name__="up",namespace=~"default|something"}`}, expResponse: okResponse, }, } { for _, endpoint := range []string{"series"} { t.Run(endpoint+"/"+strings.ReplaceAll(tc.name, " ", "_"), func(t *testing.T) { m := newMockUpstream( checkParameterAbsent( proxyLabel, checkQueryHandler("", matchersParam, tc.expMatch...), ), ) defer m.Close() r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}) if err != nil { t.Fatalf("unexpected error: %v", err) } u, err := url.Parse("http://prometheus.example.com/api/v1/" + endpoint) if err != nil { t.Fatalf("unexpected error: %v", err) } q := u.Query() if tc.promQuery != "" { q.Add(matchersParam, tc.promQuery) } for _, lv := range tc.labelv { q.Add(proxyLabel, lv) } u.RawQuery = q.Encode() w := httptest.NewRecorder() req := httptest.NewRequest("GET", u.String(), nil) r.ServeHTTP(w, req) resp := w.Result() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("unexpected error: %v", err) } defer resp.Body.Close() if resp.StatusCode != tc.expCode { t.Logf("expected status code %d, got %d", tc.expCode, resp.StatusCode) t.Logf("%s", string(body)) t.FailNow() } if resp.StatusCode != http.StatusOK { return } if string(body) != string(tc.expResponse) { t.Fatalf("expected response body %q, got %q", string(tc.expResponse), string(body)) } }) } } } func TestSeriesWithPost(t *testing.T) { for _, tc := range []struct { name string labelv []string promQueryBody string expResponse []byte method string expCode int expMatch []string expBody []byte }{ { name: `No "namespace" parameter returns an error`, expCode: http.StatusBadRequest, }, { name: `No "namespace" parameter returns an error for POSTs`, expCode: http.StatusBadRequest, method: http.MethodPost, }, { name: `No "match[]" parameter returns 200 with empty body`, labelv: []string{"default"}, method: http.MethodPost, expMatch: []string{`{namespace="default"}`}, expResponse: okResponse, expCode: http.StatusOK, }, { name: `No "match[]" parameter returns 200 with empty body for POSTs`, method: http.MethodPost, labelv: []string{"default"}, expMatch: []string{`{namespace="default"}`}, expResponse: okResponse, expCode: http.StatusOK, }, { name: `Series POST`, labelv: []string{"default"}, promQueryBody: "up", method: http.MethodPost, expCode: http.StatusOK, expMatch: []string{`{__name__="up",namespace="default"}`}, expResponse: okResponse, }, { name: `Series POST with multiple label values`, labelv: []string{"default", "something"}, promQueryBody: "up", method: http.MethodPost, expCode: http.StatusOK, expMatch: []string{`{__name__="up",namespace=~"default|something"}`}, expResponse: okResponse, }, { name: `Series POST: check that label values are correctly escaped`, labelv: []string{"default", "some|thing"}, promQueryBody: "up", method: http.MethodPost, expCode: http.StatusOK, expMatch: []string{`{__name__="up",namespace=~"default|some\\|thing"}`}, expResponse: okResponse, }, { name: `Series with labels POST`, labelv: []string{"default"}, promQueryBody: `up{instance="localhost:9090"}`, method: http.MethodPost, expCode: http.StatusOK, expMatch: []string{`{instance="localhost:9090",__name__="up",namespace="default"}`}, expResponse: okResponse, }, { name: `Series POST with multiple label values and existing matcher`, labelv: []string{"default", "something"}, promQueryBody: `up{instance="localhost:9090",namespace="something"}`, method: http.MethodPost, expCode: http.StatusOK, expMatch: []string{`{instance="localhost:9090",namespace="something",__name__="up",namespace=~"default|something"}`}, expResponse: okResponse, }, } { for _, endpoint := range []string{"series"} { t.Run(endpoint+"/"+strings.ReplaceAll(tc.name, " ", "_"), func(t *testing.T) { m := newMockUpstream( checkParameterAbsent( proxyLabel, checkFormHandler(matchersParam, tc.expMatch...), ), ) defer m.Close() r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}) if err != nil { t.Fatalf("unexpected error: %v", err) } u, err := url.Parse("http://prometheus.example.com/api/v1/" + endpoint) if err != nil { t.Fatalf("unexpected error: %v", err) } q := u.Query() for _, lv := range tc.labelv { q.Add(proxyLabel, lv) } u.RawQuery = q.Encode() var b io.Reader = nil if tc.promQueryBody != "" { b = strings.NewReader(url.Values(map[string][]string{"match[]": {tc.promQueryBody}}).Encode()) } w := httptest.NewRecorder() req := httptest.NewRequest(tc.method, u.String(), b) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.ServeHTTP(w, req) resp := w.Result() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("unexpected error: %v", err) } defer resp.Body.Close() if resp.StatusCode != tc.expCode { t.Logf("expected status code %d, got %d", tc.expCode, resp.StatusCode) t.Logf("%s", string(body)) t.FailNow() } if resp.StatusCode != http.StatusOK { return } if string(body) != string(tc.expResponse) { t.Fatalf("expected response body %q, got %q", string(tc.expResponse), string(body)) } }) } } } func TestQuery(t *testing.T) { for _, tc := range []struct { name string labelv []string headers http.Header headerName string queryParam string staticLabelVal []string promQuery string promQueryBody string method string expCode int expPromQuery string expPromQueryBody string expResponse []byte errorOnReplace bool regexMatch bool headerUsesListSyntax bool }{ { name: `No "namespace" parameter returns an error`, expCode: http.StatusBadRequest, }, { name: `No "namespace" parameter returns an error for POSTs`, expCode: http.StatusBadRequest, method: http.MethodPost, }, { labelv: []string{"default", ""}, name: `One of the "namespace" parameters empty returns 200`, expCode: http.StatusOK, }, { labelv: []string{"default", ""}, name: `One of the "namespace" parameters empty returns 200 for POSTs`, expCode: http.StatusOK, method: http.MethodPost, }, { name: `No "query" parameter returns 200 with empty body`, labelv: []string{"default"}, expCode: http.StatusOK, }, { name: `No "query" parameter returns 200 with empty body for POSTs`, labelv: []string{"default"}, expCode: http.StatusOK, method: http.MethodPost, }, { name: `Query without a vector selector`, labelv: []string{"default"}, promQuery: "up", expCode: http.StatusOK, expPromQuery: `up{namespace="default"}`, expResponse: okResponse, }, { name: `Query: check that label values are correctly escaped`, labelv: []string{"de|fault", "something"}, promQuery: "up", expCode: http.StatusOK, expPromQuery: `up{namespace=~"de\\|fault|something"}`, expResponse: okResponse, }, { name: `Query: check that label values are not escaped for single label values`, labelv: []string{"de|fault"}, promQuery: "up", expCode: http.StatusOK, expPromQuery: `up{namespace="de|fault"}`, expResponse: okResponse, }, { name: `Query without a vector selector with multiple label values`, labelv: []string{"default", "second"}, promQuery: "up", expCode: http.StatusOK, expPromQuery: `up{namespace=~"default|second"}`, expResponse: okResponse, }, { name: `Query without a vector selector in POST body`, labelv: []string{"default"}, promQueryBody: "up", method: http.MethodPost, expCode: http.StatusOK, expPromQueryBody: `up{namespace="default"}`, expResponse: okResponse, }, { name: `Query without a vector selector in POST body with multiple label values`, labelv: []string{"default", "second"}, promQueryBody: "up", method: http.MethodPost, expCode: http.StatusOK, expPromQueryBody: `up{namespace=~"default|second"}`, expResponse: okResponse, }, { name: `Tricky: Query without a vector selector in GET body (yes, that's possible)'`, labelv: []string{"default"}, promQueryBody: "up", method: http.MethodGet, expCode: http.StatusOK, expPromQueryBody: ``, // We should finish request without forwarding. Form should not parse this value for GET. }, { name: `Query without a vector selector in POST body or query`, labelv: []string{"default"}, promQuery: "up", promQueryBody: "up", method: http.MethodPost, expCode: http.StatusOK, expPromQuery: `up{namespace="default"}`, expPromQueryBody: `up{namespace="default"}`, expResponse: okResponse, }, { name: `Query without a vector selector in POST body or query with multiple label values`, labelv: []string{"default", "second"}, promQuery: "up", promQueryBody: "up", method: http.MethodPost, expCode: http.StatusOK, expPromQuery: `up{namespace=~"default|second"}`, expPromQueryBody: `up{namespace=~"default|second"}`, expResponse: okResponse, }, { name: `Query without a vector selector in POST body or query different`, labelv: []string{"default"}, promQuery: "up", promQueryBody: "foo", method: http.MethodPost, expCode: http.StatusOK, expPromQuery: `up{namespace="default"}`, expPromQueryBody: `foo{namespace="default"}`, expResponse: okResponse, }, { name: `Query without a vector selector in POST body or query different with multiple label values`, labelv: []string{"default", "second"}, promQuery: "up", promQueryBody: "foo", method: http.MethodPost, expCode: http.StatusOK, expPromQuery: `up{namespace=~"default|second"}`, expPromQueryBody: `foo{namespace=~"default|second"}`, expResponse: okResponse, }, { name: `Query with a vector selector`, labelv: []string{"default"}, promQuery: `up{namespace="other"}`, expCode: http.StatusOK, expPromQuery: `up{namespace="default"}`, expResponse: okResponse, }, { name: `Query with a vector selector with multiple label values`, labelv: []string{"default", "second"}, promQuery: `up{namespace="second"}`, expCode: http.StatusOK, expPromQuery: `up{namespace="second",namespace=~"default|second"}`, expResponse: okResponse, }, { name: `Query with a vector selector with empty label values`, labelv: []string{"default", ""}, promQuery: `up{namespace="other"}`, expCode: http.StatusOK, expPromQuery: `up{namespace="default"}`, expResponse: okResponse, }, { name: `Query with a vector selector in POST body`, labelv: []string{"default"}, promQueryBody: `up{namespace="other"}`, method: http.MethodPost, expCode: http.StatusOK, expPromQueryBody: `up{namespace="default"}`, expResponse: okResponse, }, { name: `Query with a vector selector in POST body with multiple label values`, labelv: []string{"default", "second"}, promQueryBody: `up{namespace="second"}`, method: http.MethodPost, expCode: http.StatusOK, expPromQueryBody: `up{namespace="second",namespace=~"default|second"}`, expResponse: okResponse, }, { name: `Query with a vector selector and errorOnReplace`, labelv: []string{"default"}, promQuery: `up{namespace="other"}`, errorOnReplace: true, expCode: http.StatusBadRequest, expResponse: nil, }, { name: `Query with a vector selector, multiple values and errorOnReplace`, labelv: []string{"default", "default2"}, promQuery: `up{namespace="other"}`, errorOnReplace: true, expCode: http.StatusBadRequest, expResponse: nil, }, { name: `Query with a vector selector in POST body and errorOnReplace`, labelv: []string{"default"}, promQueryBody: `up{namespace="other"}`, method: http.MethodPost, errorOnReplace: true, expCode: http.StatusBadRequest, expResponse: nil, }, { name: `Query with a scalar`, labelv: []string{"default"}, promQuery: "1", expCode: http.StatusOK, expPromQuery: `1`, expResponse: okResponse, }, { name: `Query with a scalar in POST body`, labelv: []string{"default"}, promQueryBody: "1", method: http.MethodPost, expCode: http.StatusOK, expPromQueryBody: `1`, expResponse: okResponse, }, { name: `Query with a function`, labelv: []string{"default"}, promQuery: "time()", expCode: http.StatusOK, expPromQuery: `time()`, expResponse: okResponse, }, { name: `Query with a function in POST body`, labelv: []string{"default"}, promQueryBody: "time()", method: http.MethodPost, expCode: http.StatusOK, expPromQueryBody: `time()`, expResponse: okResponse, }, { name: `An invalid expression returns 400 with error response`, labelv: []string{"default"}, promQuery: "up +", expCode: http.StatusBadRequest, }, { name: `An invalid expression in POST body returns 400 with error response`, labelv: []string{"default"}, promQueryBody: "up +", method: http.MethodPost, expCode: http.StatusBadRequest, }, { name: `Binary expression`, labelv: []string{"default"}, promQuery: `up{instance="localhost:9090"} + foo{namespace="other"}`, expCode: http.StatusOK, expPromQuery: `up{instance="localhost:9090",namespace="default"} + foo{namespace="default"}`, expResponse: okResponse, }, { name: `Binary expression with multiple label values`, labelv: []string{"default", "second"}, promQuery: `up{instance="localhost:9090"} + foo{namespace="second"}`, expCode: http.StatusOK, expPromQuery: `up{instance="localhost:9090",namespace=~"default|second"} + foo{namespace="second",namespace=~"default|second"}`, expResponse: okResponse, }, { name: `Static label value`, staticLabelVal: []string{"default"}, promQuery: `up{instance="localhost:9090"} + foo{namespace="default"}`, expCode: http.StatusOK, expPromQuery: `up{instance="localhost:9090",namespace="default"} + foo{namespace="default"}`, expResponse: okResponse, }, { name: `Multiple static label values`, staticLabelVal: []string{"default", "second"}, promQuery: `up{instance="localhost:9090"} + foo{namespace="second"}`, expCode: http.StatusOK, expPromQuery: `up{instance="localhost:9090",namespace=~"default|second"} + foo{namespace="second",namespace=~"default|second"}`, expResponse: okResponse, }, { name: `HTTP header label value`, headers: http.Header{"namespace": []string{"default"}}, headerName: "namespace", promQuery: `up{instance="localhost:9090"} + foo{namespace="other"}`, expCode: http.StatusOK, expPromQuery: `up{instance="localhost:9090",namespace="default"} + foo{namespace="default"}`, expResponse: okResponse, }, { name: `multiple HTTP header label values`, headers: http.Header{"namespace": []string{"default", "second"}}, headerName: "namespace", promQuery: `up{instance="localhost:9090"} + foo{namespace="second"}`, expCode: http.StatusOK, expPromQuery: `up{instance="localhost:9090",namespace=~"default|second"} + foo{namespace="second",namespace=~"default|second"}`, expResponse: okResponse, }, { name: `HTTP header label with comma-separated values and list parsing disabled`, headers: http.Header{"namespace": []string{"default, second", "third"}}, headerName: "namespace", promQuery: `up{instance="localhost:9090"} + foo{namespace="second"}`, expCode: http.StatusOK, expPromQuery: `up{instance="localhost:9090",namespace=~"default, second|third"} + foo{namespace="second",namespace=~"default, second|third"}`, expResponse: okResponse, headerUsesListSyntax: false, }, { name: `HTTP header label with comma-separated values and list parsing enabled`, headers: http.Header{"namespace": []string{"default, second", "third"}}, headerName: "namespace", promQuery: `up{instance="localhost:9090"} + foo{namespace="second"}`, expCode: http.StatusOK, expPromQuery: `up{instance="localhost:9090",namespace=~"default|second|third"} + foo{namespace="second",namespace=~"default|second|third"}`, expResponse: okResponse, headerUsesListSyntax: true, }, { name: `multiple HTTP header with empty label value`, headers: http.Header{"namespace": []string{"default", ""}}, headerName: "namespace", promQuery: `up{instance="localhost:9090"} + foo{namespace="other"}`, expCode: http.StatusOK, expPromQuery: `up{instance="localhost:9090",namespace="default"} + foo{namespace="default"}`, expResponse: okResponse, }, { name: `query param label value`, queryParam: "namespace2", labelv: []string{"default"}, promQuery: `up{instance="localhost:9090"} + foo{namespace="other"}`, expCode: http.StatusOK, expPromQuery: `up{instance="localhost:9090",namespace="default"} + foo{namespace="default"}`, expResponse: okResponse, }, { name: `HTTP header as regexp`, headers: http.Header{"namespace": []string{"tenant1-.*"}}, headerName: "namespace", regexMatch: true, promQuery: `up{instance="localhost:9090"} + foo{namespace="other"}`, expCode: http.StatusOK, expPromQuery: `up{instance="localhost:9090",namespace=~"tenant1-.*"} + foo{namespace="other",namespace=~"tenant1-.*"}`, expResponse: okResponse, }, { name: `query param as regexp`, queryParam: "namespace", labelv: []string{"tenant1-.*"}, regexMatch: true, promQuery: `up{instance="localhost:9090"} + foo{namespace="other"}`, expCode: http.StatusOK, expPromQuery: `up{instance="localhost:9090",namespace=~"tenant1-.*"} + foo{namespace="other",namespace=~"tenant1-.*"}`, expResponse: okResponse, }, { name: `HTTP header as regexp with same regexp in query`, headers: http.Header{"namespace": []string{"tenant1-.*"}}, headerName: "namespace", regexMatch: true, promQuery: `up{instance="localhost:9090"} + foo{namespace="tenant1-.*"}`, expCode: http.StatusOK, expPromQuery: `up{instance="localhost:9090",namespace=~"tenant1-.*"} + foo{namespace="tenant1-.*",namespace=~"tenant1-.*"}`, expResponse: okResponse, }, { name: `HTTP header with invalid regexp with same regexp in query`, headers: http.Header{"namespace": []string{"tenant1-(.*"}}, headerName: "namespace", regexMatch: true, promQuery: `up{instance="localhost:9090"} + foo{namespace="tenant1-.*"}`, expCode: http.StatusBadRequest, }, { name: `Multiple regexp HTTP headers is invalid`, headers: http.Header{"namespace": []string{"tenant1", "tenant2"}}, headerName: "namespace", regexMatch: true, promQuery: `up{instance="localhost:9090"} + foo{namespace="tenant1-.*"}`, expCode: http.StatusBadRequest, }, { name: `Regex should not match empty string`, headers: http.Header{"namespace": []string{".*"}}, headerName: "namespace", regexMatch: true, promQuery: `up{instance="localhost:9090"} + foo{namespace="tenant1-.*"}`, expCode: http.StatusBadRequest, }, } { for _, endpoint := range []string{"query", "query_range", "query_exemplars"} { t.Run(endpoint+"/"+strings.ReplaceAll(tc.name, " ", "_"), func(t *testing.T) { var expBody string if tc.expPromQueryBody != "" { expBody = url.Values(map[string][]string{"query": {tc.expPromQueryBody}}).Encode() } mockHandler := checkQueryHandler(expBody, queryParam, tc.expPromQuery) if (len(tc.staticLabelVal) == 0) != (tc.headers == nil) { mockHandler = checkParameterAbsent(proxyLabel, mockHandler) } m := newMockUpstream(mockHandler) defer m.Close() var opts []Option if tc.errorOnReplace { opts = append(opts, WithErrorOnReplace()) } if tc.regexMatch { opts = append(opts, WithRegexMatch()) } var labelEnforcer ExtractLabeler if len(tc.staticLabelVal) > 0 { labelEnforcer = StaticLabelEnforcer(tc.staticLabelVal) } else if tc.headerName != "" { labelEnforcer = HTTPHeaderEnforcer{Name: tc.headerName, ParseListSyntax: tc.headerUsesListSyntax} } else if tc.queryParam != "" { labelEnforcer = HTTPFormEnforcer{ParameterName: tc.queryParam} } else { labelEnforcer = HTTPFormEnforcer{ParameterName: proxyLabel} } r, err := NewRoutes(m.url, proxyLabel, labelEnforcer, opts...) if err != nil { t.Fatalf("unexpected error: %v", err) } u, err := url.Parse("http://prometheus.example.com/api/v1/" + endpoint) if err != nil { t.Fatalf("unexpected error: %v", err) } q := u.Query() q.Set(queryParam, tc.promQuery) if tc.queryParam != "" { for _, lv := range tc.labelv { q.Add(tc.queryParam, lv) } } else if len(tc.staticLabelVal) == 0 && tc.headerName == "" && len(tc.labelv) > 0 { for _, lv := range tc.labelv { q.Add(proxyLabel, lv) } } u.RawQuery = q.Encode() var b io.Reader = nil if tc.promQueryBody != "" { b = strings.NewReader(url.Values(map[string][]string{"query": {tc.promQueryBody}}).Encode()) } w := httptest.NewRecorder() req := httptest.NewRequest(tc.method, u.String(), b) if tc.headers != nil { req.Header = tc.headers } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.ServeHTTP(w, req) resp := w.Result() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("unexpected error: %v", err) } defer resp.Body.Close() if resp.StatusCode != tc.expCode { t.Logf("expected status code %d, got %d", tc.expCode, resp.StatusCode) t.Logf("%s", string(body)) t.FailNow() } if resp.StatusCode != http.StatusOK { return } if string(body) != string(tc.expResponse) { t.Fatalf("expected response body %q, got %q", string(tc.expResponse), string(body)) } }) } } } ================================================ FILE: injectproxy/rules.go ================================================ // Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package injectproxy import ( "bytes" "compress/gzip" "encoding/json" "errors" "fmt" "io" "net/http" "time" "github.com/prometheus/prometheus/model/labels" ) type apiResponse struct { Status string `json:"status"` Data json.RawMessage `json:"data,omitempty"` ErrorType string `json:"errorType,omitempty"` Error string `json:"error,omitempty"` Warnings []string `json:"warnings,omitempty"` } func getAPIResponse(resp *http.Response) (*apiResponse, error) { defer resp.Body.Close() reader := resp.Body if resp.Header.Get("Content-Encoding") == "gzip" && !resp.Uncompressed { var err error reader, err = gzip.NewReader(resp.Body) if err != nil { return nil, fmt.Errorf("gzip decoding error: %w", err) } defer reader.Close() // TODO: recompress the modified response? resp.Header.Del("Content-Encoding") } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } var apir apiResponse if err := json.NewDecoder(reader).Decode(&apir); err != nil { return nil, fmt.Errorf("JSON decoding error: %w", err) } if apir.Status != "success" { return nil, fmt.Errorf("unexpected response status: %q", apir.Status) } return &apir, nil } type rulesData struct { RuleGroups []*ruleGroup `json:"groups"` } type ruleGroup struct { Name string `json:"name"` File string `json:"file"` Rules []rule `json:"rules"` Interval float64 `json:"interval"` } type rule struct { *alertingRule *recordingRule } func (r *rule) Labels() labels.Labels { if r.alertingRule != nil { return r.alertingRule.Labels } return r.recordingRule.Labels } // MarshalJSON implements the json.Marshaler interface for rule. func (r *rule) MarshalJSON() ([]byte, error) { if r.alertingRule != nil { return json.Marshal(r.alertingRule) } return json.Marshal(r.recordingRule) } // UnmarshalJSON implements the json.Unmarshaler interface for rule. func (r *rule) UnmarshalJSON(b []byte) error { var ruleType struct { Type string `json:"type"` } if err := json.Unmarshal(b, &ruleType); err != nil { return err } switch ruleType.Type { case "alerting": var alertingr alertingRule if err := json.Unmarshal(b, &alertingr); err != nil { return err } r.alertingRule = &alertingr case "recording": var recordingr recordingRule if err := json.Unmarshal(b, &recordingr); err != nil { return err } r.recordingRule = &recordingr default: return fmt.Errorf("failed to unmarshal rule: unknown type %q", ruleType.Type) } return nil } type alertingRule struct { State string `json:"state"` Name string `json:"name"` Query string `json:"query"` Duration float64 `json:"duration"` KeepFiringFor float64 `json:"keepFiringFor"` Labels labels.Labels `json:"labels"` Annotations labels.Labels `json:"annotations"` Alerts []*alert `json:"alerts"` Health string `json:"health"` LastError string `json:"lastError,omitempty"` EvaluationTime float64 `json:"evaluationTime"` LastEvaluation time.Time `json:"lastEvaluation"` // Type of an alertingRule is always "alerting". Type string `json:"type"` } type recordingRule struct { Name string `json:"name"` Query string `json:"query"` Labels labels.Labels `json:"labels,omitempty"` Health string `json:"health"` LastError string `json:"lastError,omitempty"` EvaluationTime float64 `json:"evaluationTime"` LastEvaluation time.Time `json:"lastEvaluation"` // Type of a recordingRule is always "recording". Type string `json:"type"` } type alertsData struct { Alerts []*alert `json:"alerts"` } type alert struct { Labels labels.Labels `json:"labels"` Annotations labels.Labels `json:"annotations"` State string `json:"state"` ActiveAt *time.Time `json:"activeAt,omitempty"` KeepFiringSince *time.Time `json:"keepFiringSince,omitempty"` Value string `json:"value"` } // errModifyResponseFailed is returned when the proxy failed to modify the // response from the backend. var errModifyResponseFailed = errors.New("failed to process the API response") // modifyAPIResponse unwraps the Prometheus API response, passes the enforced // label value and the response to the given function and finally replaces the // result in the response. func modifyAPIResponse(f func([]string, *http.Request, *apiResponse) (any, error)) func(*http.Response) error { return func(resp *http.Response) error { if resp.StatusCode != http.StatusOK { // Pass non-200 responses as-is. return nil } apir, err := getAPIResponse(resp) if err != nil { return fmt.Errorf("can't decode the response: %w", err) } v, err := f(MustLabelValues(resp.Request.Context()), resp.Request, apir) if err != nil { return fmt.Errorf("%w: %w", errModifyResponseFailed, err) } b, err := json.Marshal(v) if err != nil { return fmt.Errorf("can't encode the data: %w", err) } apir.Data = json.RawMessage(b) var buf bytes.Buffer if err = json.NewEncoder(&buf).Encode(apir); err != nil { return fmt.Errorf("can't encode the response: %w", err) } resp.Body = io.NopCloser(&buf) resp.Header["Content-Length"] = []string{fmt.Sprint(buf.Len())} return nil } } func (r *routes) filterRules(lvalues []string, req *http.Request, resp *apiResponse) (any, error) { var rgs rulesData if err := json.Unmarshal(resp.Data, &rgs); err != nil { return nil, fmt.Errorf("can't decode rules data: %w", err) } m, err := r.newLabelMatcher(lvalues...) if err != nil { return nil, err } filtered := []*ruleGroup{} for _, rg := range rgs.RuleGroups { var rules []rule for _, rgr := range rg.Rules { if lval := rgr.Labels().Get(r.label); lval != "" && m.Matches(lval) { rules = append(rules, rgr) continue } if !r.rulesWithActiveAlerts || rgr.alertingRule == nil { continue } var ar *alertingRule for i := range rgr.Alerts { if lval := rgr.Alerts[i].Labels.Get(r.label); lval == "" || !m.Matches(lval) { continue } if ar == nil { ar = &alertingRule{ Name: rgr.alertingRule.Name, Query: rgr.alertingRule.Query, Duration: rgr.Duration, KeepFiringFor: rgr.KeepFiringFor, Labels: rgr.alertingRule.Labels.Copy(), Annotations: rgr.Annotations.Copy(), Health: rgr.alertingRule.Health, LastError: rgr.alertingRule.LastError, EvaluationTime: rgr.alertingRule.EvaluationTime, LastEvaluation: rgr.alertingRule.LastEvaluation, Type: rgr.alertingRule.Type, } } ar.Alerts = append(ar.Alerts, rgr.Alerts[i]) switch ar.State { case "pending": if rgr.alertingRule.Alerts[i].State == "firing" { ar.State = rgr.alertingRule.Alerts[i].State } case "": ar.State = rgr.alertingRule.Alerts[i].State } } if ar != nil { rules = append(rules, rule{alertingRule: ar}) } } if len(rules) > 0 { rg.Rules = rules filtered = append(filtered, rg) } } return &rulesData{RuleGroups: filtered}, nil } func (r *routes) filterAlerts(lvalues []string, _ *http.Request, resp *apiResponse) (any, error) { var data alertsData if err := json.Unmarshal(resp.Data, &data); err != nil { return nil, fmt.Errorf("can't decode alerts data: %w", err) } m, err := r.newLabelMatcher(lvalues...) if err != nil { return nil, err } filtered := []*alert{} for _, alert := range data.Alerts { if lval := alert.Labels.Get(r.label); lval != "" && m.Matches(lval) { filtered = append(filtered, alert) } } return &alertsData{Alerts: filtered}, nil } ================================================ FILE: injectproxy/rules_test.go ================================================ // Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package injectproxy import ( "compress/gzip" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "net/url" "reflect" "testing" "gotest.tools/v3/golden" ) type gzipResponseWriter struct { io.Writer http.ResponseWriter } func (w *gzipResponseWriter) Write(b []byte) (int, error) { return w.Writer.Write(b) } func gzipHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gz := gzip.NewWriter(w) defer gz.Close() w.Header().Del("Content-Length") w.Header().Set("Content-Encoding", "gzip") next.ServeHTTP(&gzipResponseWriter{Writer: gz, ResponseWriter: w}, r) }) } func validRulesWithLabelMatchers(exp ...string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if err := req.ParseForm(); err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } matchers := req.Form["match[]"] if !reflect.DeepEqual(matchers, exp) { w.WriteHeader(http.StatusInternalServerError) w.Write(fmt.Appendf(nil, "invalid matchers:\n- expected: %q\n- got: %q", exp, matchers)) return } w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "status": "success", "data": { "groups": [ { "name": "group1", "file": "testdata/rules1.yml", "rules": [ { "name": "metric1", "query": "0", "labels": { "namespace": "ns1" }, "health": "ok", "type": "recording", "evaluationTime": 0.000214303, "lastEvaluation": "2024-04-29T14:23:52.403557247+02:00" } ], "interval": 10 } ] } }`)) }) } func validRules() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "status": "success", "data": { "groups": [ { "name": "group1", "file": "testdata/rules1.yml", "rules": [ { "name": "metric1", "query": "0", "labels": { "namespace": "ns1" }, "health": "ok", "type": "recording", "evaluationTime": 0.000214303, "lastEvaluation": "2024-04-29T14:23:52.403557247+02:00" }, { "name": "metric2", "query": "1", "labels": { "namespace": "ns1", "operation": "create" }, "health": "ok", "type": "recording", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:53.403557247+02:00" }, { "name": "metric2", "query": "0", "labels": { "namespace": "ns1", "operation": "update" }, "health": "ok", "type": "recording", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:54.403557247+02:00" }, { "name": "metric2", "query": "0", "labels": { "namespace": "ns1", "operation": "delete" }, "health": "ok", "type": "recording", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:53.603557247+02:00" }, { "state": "firing", "name": "Alert1", "query": "metric1{namespace=\"ns1\"} == 0", "duration": 0, "labels": { "namespace": "ns1" }, "annotations": {}, "alerts": [ { "labels": { "alertname": "Alert1", "namespace": "ns1" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" } ], "health": "ok", "type": "alerting", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:53.803557247+02:00" }, { "state": "firing", "name": "Alert2", "query": "metric2{namespace=\"ns1\"} == 0", "duration": 0, "labels": { "namespace": "ns1" }, "annotations": {}, "alerts": [ { "labels": { "alertname": "Alert2", "namespace": "ns1", "operation": "update" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert2", "namespace": "ns1", "operation": "delete" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" } ], "health": "ok", "type": "alerting", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:53.903557247+02:00" } ], "interval": 10 }, { "name": "group1", "file": "testdata/rules2.yml", "rules": [ { "name": "metric1", "query": "1", "labels": { "namespace": "ns2" }, "health": "ok", "type": "recording", "evaluationTime": 0.000214303, "lastEvaluation": "2024-04-29T14:23:52.403557247+02:00" }, { "state": "inactive", "name": "Alert1", "query": "metric1{namespace=\"ns2\"} == 0", "duration": 0, "labels": { "namespace": "ns2" }, "annotations": {}, "alerts": [], "health": "ok", "type": "alerting", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.503557247+02:00" } ], "interval": 10 }, { "name": "group2", "file": "testdata/rules2.yml", "rules": [ { "name": "metric2", "query": "1", "labels": { "namespace": "ns2", "operation": "create" }, "health": "ok", "type": "recording", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.503557247+02:00" }, { "name": "metric2", "query": "2", "labels": { "namespace": "ns2", "operation": "update" }, "health": "ok", "type": "recording", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.603557247+02:00" }, { "name": "metric2", "query": "3", "labels": { "namespace": "ns2", "operation": "delete" }, "health": "ok", "type": "recording", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.643557247+02:00" }, { "name": "metric3", "query": "0", "labels": { "namespace": "ns2" }, "health": "ok", "type": "recording", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.683557247+02:00" }, { "state": "inactive", "name": "Alert2", "query": "metric2{namespace=\"ns2\"} == 0", "duration": 0, "labels": { "namespace": "ns2" }, "annotations": {}, "alerts": [], "health": "ok", "type": "alerting", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.803557247+02:00" }, { "state": "firing", "name": "Alert3", "query": "metric3{namespace=\"ns2\"} == 0", "duration": 0, "labels": { "namespace": "ns2" }, "annotations": {}, "alerts": [ { "labels": { "alertname": "Alert3", "namespace": "ns2" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:39.972915521+01:00", "value": "0e+00" } ], "health": "ok", "type": "alerting", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.903557247+02:00" } ], "interval": 10 }, { "name": "group3", "file": "testdata/rules3.yml", "rules": [ { "state": "firing", "name": "Alert3", "query": "metric4{ns!=\"default\"} == 0", "duration": 300, "labels": {}, "annotations": {}, "alerts": [ { "labels": { "alertname": "Alert3", "namespace": "ns1" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:39.972915521+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert3", "namespace": "ns3" }, "annotations": {}, "state": "pending", "activeAt": "2019-12-18T13:20:39.972915521+01:00", "value": "0e+00" } ], "health": "ok", "type": "alerting", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.903557247+02:00" }, { "state": "firing", "name": "Alert4", "query": "metric5 == 0", "duration": 300, "labels": {}, "annotations": {}, "alerts": [ { "labels": { "alertname": "Alert4", "namespace": "ns3", "state": "foo" }, "annotations": {}, "state": "pending", "activeAt": "2019-12-18T13:20:39.972915521+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert1", "namespace": "ns3", "state": "bar" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:39.972915521+01:00", "value": "0e+00" } ], "health": "ok", "type": "alerting", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.903557247+02:00" } ], "interval": 10 } ] } }`)) }) } func validAlerts() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "status": "success", "data": { "alerts": [ { "labels": { "alertname": "Alert1", "namespace": "ns1" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert2", "namespace": "ns1", "operation": "update" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert2", "namespace": "ns1", "operation": "delete" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert3", "namespace": "ns2" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:39.972915521+01:00", "value": "0e+00" } ] } }`)) }) } func TestRules(t *testing.T) { for _, tc := range []struct { labelv []string upstream http.Handler reqHeaders http.Header opts []Option expCode int golden string }{ { // No "namespace" parameter returns an error. expCode: http.StatusBadRequest, golden: "rules_no_namespace_error.golden", }, { // non 200 status code from upstream is passed as-is. labelv: []string{"upstream_error"}, upstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("error")) }), expCode: http.StatusBadRequest, golden: "rules_upstream_error.golden", }, { // incomplete API response triggers a 502 error. labelv: []string{"incomplete_data_from_upstream"}, upstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("{")) }), expCode: http.StatusBadGateway, golden: "rules_incomplete_upstream_response.golden", }, { // invalid API response triggers a 502 error. labelv: []string{"invalid_data_from_upstream"}, upstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("0")) }), expCode: http.StatusBadGateway, golden: "rules_invalid_upstream_response.golden", }, { // "namespace" parameter matching no rule. labelv: []string{"not_present"}, upstream: validRules(), expCode: http.StatusOK, golden: "rules_no_match.golden", }, { // Gzipped response should be handled when explictly asked by the original client. labelv: []string{"not_present_gzip_requested"}, upstream: gzipHandler(validRules()), reqHeaders: map[string][]string{ "Accept-Encoding": {"gzip"}, }, expCode: http.StatusOK, golden: "rules_no_match_with_gzip_requested.golden", }, { // When the client doesn't ask explicitly for gzip encoding, the Go // standard library will automatically ask for it and it will // transparently decompress the gzipped response. labelv: []string{"not_present_gzip_not_requested"}, upstream: gzipHandler(validRules()), expCode: http.StatusOK, golden: "rules_no_match_with_gzip_not_requested.golden", }, { labelv: []string{"ns1"}, upstream: validRules(), expCode: http.StatusOK, golden: "rules_match_namespace_ns1.golden", }, { labelv: []string{"ns2"}, upstream: validRules(), expCode: http.StatusOK, golden: "rules_match_namespace_ns2.golden", }, { labelv: []string{"ns1", "ns2"}, upstream: validRules(), expCode: http.StatusOK, golden: "rules_match_namespaces_ns1_and_ns2.golden", }, { labelv: []string{"ns1|ns2"}, upstream: validRules(), opts: []Option{WithRegexMatch()}, expCode: http.StatusOK, golden: "rules_match_namespaces_ns1_and_ns2.golden", }, { labelv: []string{"ns1|ns2", "ns3"}, upstream: validRules(), opts: []Option{WithRegexMatch()}, expCode: http.StatusBadRequest, golden: "rules_invalid_upstream_response.golden", }, { labelv: []string{"ns3"}, upstream: validRules(), opts: []Option{WithActiveAlerts()}, expCode: http.StatusOK, golden: "rules_with_active_alerts.golden", }, { labelv: []string{"ns1"}, upstream: validRulesWithLabelMatchers("{namespace=\"ns1\"}"), opts: []Option{WithLabelMatchersForRulesAPI()}, expCode: http.StatusOK, golden: "rules_with_label_matchers.golden", }, } { t.Run(fmt.Sprintf("%s=%s", proxyLabel, tc.labelv), func(t *testing.T) { m := newMockUpstream(tc.upstream) defer m.Close() r, err := NewRoutes( m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, tc.opts..., ) if err != nil { t.Fatalf("unexpected error: %v", err) } u, err := url.Parse("http://prometheus.example.com/api/v1/rules") if err != nil { t.Fatalf("unexpected error: %v", err) } q := u.Query() for _, lv := range tc.labelv { q.Add(proxyLabel, lv) } u.RawQuery = q.Encode() w := httptest.NewRecorder() req := httptest.NewRequest("GET", u.String(), nil) for k, v := range tc.reqHeaders { for i := range v { req.Header.Add(k, v[i]) } } r.ServeHTTP(w, req) resp := w.Result() if resp.StatusCode != tc.expCode { b, err := io.ReadAll(resp.Body) if err == nil { t.Logf("body: %s", b) } t.Fatalf("expected status code %d, got %d", tc.expCode, resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("expected no error, got %s", err) } if resp.StatusCode != http.StatusOK { golden.Assert(t, string(body), tc.golden) return } // We need to unmarshal/marshal the result to run deterministic comparisons. got := normalizeAPIResponse(t, body) golden.Assert(t, got, tc.golden) }) } } func TestAlerts(t *testing.T) { for _, tc := range []struct { labelv []string upstream http.Handler opts []Option expCode int golden string }{ { // No "namespace" parameter returns an error. expCode: http.StatusBadRequest, golden: "alerts_no_namespace_error.golden", }, { // non 200 status code from upstream is passed as-is. labelv: []string{"upstream_error"}, upstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte("error")) }), expCode: http.StatusBadRequest, golden: "alerts_upstream_error.golden", }, { // incomplete API response triggers a 502 error. labelv: []string{"incomplete_data_from_upstream"}, upstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("{")) }), expCode: http.StatusBadGateway, golden: "alerts_incomplete_upstream_response.golden", }, { // invalid API response triggers a 502 error. labelv: []string{"invalid_data_from_upstream"}, upstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("0")) }), expCode: http.StatusBadGateway, golden: "alerts_invalid_upstream_response.golden", }, { // "namespace" parameter matching no rule. labelv: []string{"not_present"}, upstream: validAlerts(), expCode: http.StatusOK, golden: "alerts_no_match.golden", }, { labelv: []string{"ns1"}, upstream: validAlerts(), expCode: http.StatusOK, golden: "alerts_match_namespace_ns1.golden", }, { labelv: []string{"ns2"}, upstream: validAlerts(), expCode: http.StatusOK, golden: "alerts_match_namespace_ns2.golden", }, { labelv: []string{"ns1", "ns2"}, upstream: validAlerts(), expCode: http.StatusOK, golden: "alerts_match_namespaces_ns1_and_ns2.golden", }, { labelv: []string{"ns1|ns2"}, upstream: validAlerts(), opts: []Option{WithRegexMatch()}, expCode: http.StatusOK, golden: "alerts_match_namespaces_ns1_and_ns2.golden", }, { labelv: []string{"ns1", "ns2"}, upstream: validAlerts(), opts: []Option{WithRegexMatch()}, expCode: http.StatusBadRequest, golden: "alerts_invalid_upstream_response.golden", }, } { t.Run(fmt.Sprintf("%s=%#v", proxyLabel, tc.labelv), func(t *testing.T) { m := newMockUpstream(tc.upstream) defer m.Close() r, err := NewRoutes( m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, tc.opts..., ) if err != nil { t.Fatalf("unexpected error: %v", err) } u, err := url.Parse("http://prometheus.example.com/api/v1/alerts") if err != nil { t.Fatalf("unexpected error: %v", err) } q := u.Query() for _, lv := range tc.labelv { q.Add(proxyLabel, lv) } u.RawQuery = q.Encode() w := httptest.NewRecorder() req := httptest.NewRequest("GET", u.String(), nil) r.ServeHTTP(w, req) resp := w.Result() if resp.StatusCode != tc.expCode { t.Fatalf("expected status code %d, got %d", tc.expCode, resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("expected no error, got %s", err) } if resp.StatusCode != http.StatusOK { golden.Assert(t, string(body), tc.golden) return } // We need to unmarshal/marshal the result to run deterministic comparisons. got := normalizeAPIResponse(t, body) golden.Assert(t, got, tc.golden) }) } } func normalizeAPIResponse(t *testing.T, b []byte) string { t.Helper() var apir apiResponse if err := json.Unmarshal(b, &apir); err != nil { t.Fatalf("unexpected error: %v", err) } out, err := json.MarshalIndent(&apir, "", " ") if err != nil { t.Fatalf("unexpected error: %v", err) } return string(out) } ================================================ FILE: injectproxy/silences.go ================================================ // Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package injectproxy import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "path" "regexp" "strconv" "strings" runtimeclient "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" "github.com/prometheus/alertmanager/api/v2/client" "github.com/prometheus/alertmanager/api/v2/client/silence" "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/pkg/labels" ) // silences proxies HTTP requests to the Alertmanager /api/v2/silences endpoint. func (r *routes) silences(w http.ResponseWriter, req *http.Request) { switch req.Method { case "GET": r.enforceFilterParameter(w, req) case "POST": r.postSilence(w, req) default: http.NotFound(w, req) } } // assertSingleLabelValue verifies that the proxy is configured to match only // one label value. If not, it will reply with "422 Unprocessable Content". func assertSingleLabelValue(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { labelValues := MustLabelValues(req.Context()) if len(labelValues) > 1 { http.Error(w, "Multiple label matchers not supported", http.StatusUnprocessableEntity) return } next(w, req) } } // enforceFilterParameter injects a label matcher parameter into the // Alertmanager API's query. func (r *routes) enforceFilterParameter(w http.ResponseWriter, req *http.Request) { var ( q = req.URL.Query() proxyLabelMatch labels.Matcher ) if len(MustLabelValues(req.Context())) > 1 { proxyLabelMatch = labels.Matcher{ Type: labels.MatchRegexp, Name: r.label, Value: labelValuesToRegexpString(MustLabelValues(req.Context())), } } else { matcherType := labels.MatchEqual matcherValue := MustLabelValue(req.Context()) if r.regexMatch { compiledRegex, err := regexp.Compile(matcherValue) if err != nil { prometheusAPIError(w, err.Error(), http.StatusBadRequest) return } if compiledRegex.MatchString("") { prometheusAPIError(w, "Regex should not match empty string", http.StatusBadRequest) return } matcherType = labels.MatchRegexp } proxyLabelMatch = labels.Matcher{ Type: matcherType, Name: r.label, Value: matcherValue, } } modified := []string{proxyLabelMatch.String()} for _, filter := range q["filter"] { m, err := labels.ParseMatcher(filter) if err != nil { prometheusAPIError(w, fmt.Sprintf("bad request: can't parse filter %q: %v", filter, err), http.StatusBadRequest) return } // Keep the original matcher in case of multi label values because // the user might want to filter on a specific value. if m.Name == r.label && proxyLabelMatch.Type != labels.MatchRegexp { continue } modified = append(modified, filter) } q["filter"] = modified q.Del(r.label) req.URL.RawQuery = q.Encode() r.handler.ServeHTTP(w, req) } func (r *routes) postSilence(w http.ResponseWriter, req *http.Request) { var ( sil models.PostableSilence lvalue = MustLabelValue(req.Context()) ) if err := json.NewDecoder(req.Body).Decode(&sil); err != nil { prometheusAPIError(w, fmt.Sprintf("bad request: can't decode: %v", err), http.StatusBadRequest) return } if sil.ID != "" { // This is an update for an existing silence. existing, err := r.getSilenceByID(req.Context(), sil.ID) if err != nil { prometheusAPIError(w, fmt.Sprintf("proxy error: can't get silence: %v", err), http.StatusBadGateway) return } if !hasMatcherForLabel(existing.Matchers, r.label, lvalue) { prometheusAPIError(w, "forbidden", http.StatusForbidden) return } } var falsy bool modified := models.Matchers{ &models.Matcher{Name: &(r.label), Value: &lvalue, IsRegex: &falsy}, } for _, m := range sil.Matchers { if m.Name != nil && *m.Name == r.label { continue } modified = append(modified, m) } // At least one matcher in addition to the enforced label is required, // otherwise all alerts would be silenced if len(modified) < 2 { prometheusAPIError(w, "need at least one matcher, got none", http.StatusBadRequest) return } sil.Matchers = modified var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(&sil); err != nil { prometheusAPIError(w, fmt.Sprintf("can't encode: %v", err), http.StatusInternalServerError) return } req = req.Clone(req.Context()) req.Body = io.NopCloser(&buf) req.URL.RawQuery = "" req.Header["Content-Length"] = []string{strconv.Itoa(buf.Len())} req.ContentLength = int64(buf.Len()) r.handler.ServeHTTP(w, req) } // deleteSilence proxies HTTP requests to the Alertmanager /api/v2/silence/ endpoint. func (r *routes) deleteSilence(w http.ResponseWriter, req *http.Request) { silID := strings.TrimPrefix(req.URL.Path, "/api/v2/silence/") if silID == "" || silID == req.URL.Path { prometheusAPIError(w, "bad request", http.StatusBadRequest) return } // Get the silence by ID and verify that it has the expected label. sil, err := r.getSilenceByID(req.Context(), silID) if err != nil { prometheusAPIError(w, fmt.Sprintf("proxy error: %v", err), http.StatusBadGateway) return } if !hasMatcherForLabel(sil.Matchers, r.label, MustLabelValue(req.Context())) { prometheusAPIError(w, "forbidden", http.StatusForbidden) return } req.URL.RawQuery = "" r.handler.ServeHTTP(w, req) } func (r *routes) getSilenceByID(ctx context.Context, id string) (*models.GettableSilence, error) { amc := client.New( runtimeclient.New(r.upstream.Host, path.Join(r.upstream.Path, "/api/v2"), []string{r.upstream.Scheme}), strfmt.Default, ) params := silence.NewGetSilenceParams().WithContext(ctx) params.SetSilenceID(strfmt.UUID(id)) sil, err := amc.Silence.GetSilence(params) if err != nil { return nil, err } return sil.Payload, nil } func hasMatcherForLabel(matchers models.Matchers, name, value string) bool { for _, m := range matchers { if *m.Name == name && !*m.IsRegex && *m.Value == value { return true } } return false } ================================================ FILE: injectproxy/silences_test.go ================================================ // Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package injectproxy import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/prometheus/alertmanager/api/v2/models" ) func TestListSilences(t *testing.T) { for _, tc := range []struct { labelv []string filters []string regexMatch bool expCode int expFilters []string expBody []byte }{ { // No "namespace" parameter returns an error. expCode: http.StatusBadRequest, }, { // No "filter" parameter. labelv: []string{"default"}, expCode: http.StatusOK, expFilters: []string{`namespace="default"`}, expBody: okResponse, }, { // Many "filter" parameters. labelv: []string{"default"}, filters: []string{`job="prometheus"`, `instance=~".+"`}, expCode: http.StatusOK, expFilters: []string{`job="prometheus"`, `instance=~".+"`, `namespace="default"`}, expBody: okResponse, }, { // Many "filter" parameters with a "namespace" label that needs to be enforced. labelv: []string{"default"}, filters: []string{`namespace=~"foo|default"`, `job="prometheus"`}, expCode: http.StatusOK, expFilters: []string{`namespace="default"`, `job="prometheus"`}, expBody: okResponse, }, { // Invalid "filter" parameter. labelv: []string{"default"}, filters: []string{`namespace=~"foo|default"`, `job="promethe`}, expCode: http.StatusBadRequest, }, { // Multiple label values are not supported. labelv: []string{"default", "something"}, expCode: http.StatusUnprocessableEntity, }, { // Regex match labelv: []string{"tenant1-.*"}, regexMatch: true, filters: []string{`namespace=~"foo|default"`, `job="prometheus"`}, expCode: http.StatusNotImplemented, }, } { t.Run(strings.Join(tc.filters, "&"), func(t *testing.T) { m := newMockUpstream(checkQueryHandler("", "filter", tc.expFilters...)) defer m.Close() var opts []Option if tc.regexMatch { opts = append(opts, WithRegexMatch()) } r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, opts...) if err != nil { t.Fatalf("unexpected error: %v", err) } u, err := url.Parse("http://alertmanager.example.com/api/v2/silences") if err != nil { t.Fatalf("unexpected error: %v", err) } q := u.Query() for _, m := range tc.filters { q.Add("filter", m) } for _, s := range tc.labelv { q.Add(proxyLabel, s) } u.RawQuery = q.Encode() w := httptest.NewRecorder() req := httptest.NewRequest("GET", u.String(), nil) r.ServeHTTP(w, req) resp := w.Result() body, _ := io.ReadAll(resp.Body) defer resp.Body.Close() if resp.StatusCode != tc.expCode { t.Logf("expected status code %d, got %d", tc.expCode, resp.StatusCode) t.Logf("%s", string(body)) t.FailNow() } if resp.StatusCode != http.StatusOK { return } if string(body) != string(tc.expBody) { t.Fatalf("expected body %q, got %q", string(tc.expBody), string(body)) } }) } } const silID = "802146e0-1f7a-42a6-ab0e-1e631479970b" func getSilenceWithoutLabel() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if req.Method != "GET" { prometheusAPIError(w, "invalid method: "+req.Method, http.StatusInternalServerError) return } if req.URL.Path != "/api/v2/silence/"+silID { prometheusAPIError(w, "invalid path: "+req.URL.Path, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, ` { "id": "%s", "status": { "state": "pending" }, "updatedAt": "2020-01-15T09:06:23.419Z", "comment": "comment", "createdBy": "author", "endsAt": "2020-02-13T13:00:02.084Z", "matchers": [ { "isRegex": false, "name": "foo", "value": "bar" } ], "startsAt": "2020-02-13T12:02:01.000Z" } `, silID) }) } func getSilenceWithLabel(labelv string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if req.Method != "GET" { prometheusAPIError(w, "invalid method: "+req.Method, http.StatusInternalServerError) return } if req.URL.Path != "/api/v2/silence/"+silID { prometheusAPIError(w, "invalid path: "+req.URL.Path, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, ` { "id": "%s", "status": { "state": "pending" }, "updatedAt": "2020-01-15T09:06:23.419Z", "comment": "comment", "createdBy": "author", "endsAt": "2020-02-13T13:00:02.084Z", "matchers": [ { "isRegex": false, "name": "%s", "value": "%s" } ], "startsAt": "2020-02-13T12:02:01.000Z" } `, silID, proxyLabel, labelv) }) } func createSilenceWithLabel(labelv string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { var sil models.PostableSilence if err := json.NewDecoder(req.Body).Decode(&sil); err != nil { prometheusAPIError(w, fmt.Sprintf("unexpected error: %v", err), http.StatusInternalServerError) return } var values []string for _, m := range sil.Matchers { if *m.Name == proxyLabel { values = append(values, *m.Value) } } if len(values) != 1 { prometheusAPIError(w, fmt.Sprintf("expected 1 matcher for label %s, got %d", proxyLabel, len(values)), http.StatusInternalServerError) return } if values[0] != labelv { prometheusAPIError(w, fmt.Sprintf("expected matcher for label %s to be %q, got %q", proxyLabel, labelv, values[0]), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) w.Write(okResponse) }) } // chainedHandlers runs the handler one after the other. type chainedHandlers struct { idx int handlers []http.Handler } func (c *chainedHandlers) ServeHTTP(w http.ResponseWriter, req *http.Request) { defer func() { c.idx++ }() if c.idx >= len(c.handlers) { prometheusAPIError(w, "", http.StatusInternalServerError) return } c.handlers[c.idx].ServeHTTP(w, req) } func TestDeleteSilence(t *testing.T) { for _, tc := range []struct { ID string labelv []string upstream http.Handler regexMatch bool expCode int expBody []byte }{ { // No "namespace" parameter returns an error. expCode: http.StatusBadRequest, }, { // Missing silence ID. ID: "", labelv: []string{"default"}, expCode: http.StatusBadRequest, }, { // The silence doesn't exist upstream. ID: silID, labelv: []string{"default"}, upstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { http.NotFound(w, req) }), expCode: http.StatusBadGateway, }, { // The silence doesn't contain the expected label. ID: silID, labelv: []string{"default"}, upstream: getSilenceWithoutLabel(), expCode: http.StatusForbidden, }, { // The silence doesn't have the expected value for the label. ID: silID, labelv: []string{"default"}, upstream: getSilenceWithLabel("not default"), expCode: http.StatusForbidden, }, { // The silence has the expected value for the label. ID: silID, labelv: []string{"default"}, upstream: &chainedHandlers{ handlers: []http.Handler{ getSilenceWithLabel("default"), http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("ok")) }), }, }, expCode: http.StatusOK, expBody: []byte("ok"), }, { // The silence has the expected value for the label but upstream returns an error. ID: silID, labelv: []string{"default"}, upstream: &chainedHandlers{ handlers: []http.Handler{ getSilenceWithLabel("default"), http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusTeapot) }), }, }, expCode: http.StatusTeapot, }, { // Multiple label values are not supported. labelv: []string{"default", "something"}, expCode: http.StatusUnprocessableEntity, }, { // Regexp is not supported. labelv: []string{"default"}, regexMatch: true, expCode: http.StatusNotImplemented, }, } { t.Run("", func(t *testing.T) { m := newMockUpstream(tc.upstream) defer m.Close() var opts []Option if tc.regexMatch { opts = append(opts, WithRegexMatch()) } r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, opts...) if err != nil { t.Fatalf("unexpected error: %v", err) } u, err := url.Parse(fmt.Sprintf("http://alertmanager.example.com/api/v2/silence/%s", tc.ID)) if err != nil { t.Fatalf("unexpected error: %v", err) } q := u.Query() for _, s := range tc.labelv { q.Add(proxyLabel, s) } u.RawQuery = q.Encode() w := httptest.NewRecorder() req := httptest.NewRequest("DELETE", u.String(), nil) r.ServeHTTP(w, req) resp := w.Result() body, _ := io.ReadAll(resp.Body) defer resp.Body.Close() if resp.StatusCode != tc.expCode { t.Logf("expected status code %d, got %d", tc.expCode, resp.StatusCode) t.Logf("%s", string(body)) t.FailNow() } if resp.StatusCode != http.StatusOK { return } if string(body) != string(tc.expBody) { t.Fatalf("expected body %q, got %q", string(tc.expBody), string(body)) } }) } } func TestUpdateSilence(t *testing.T) { for _, tc := range []struct { data string labelv []string upstream http.Handler expCode int expBody []byte }{ { // No "namespace" parameter returns an error. expCode: http.StatusBadRequest, }, { // Invalid silence payload returns an error. data: "{", labelv: []string{"default"}, expCode: http.StatusBadRequest, }, { // Creation of a valid silence without namespace label is ok. data: `{ "comment":"foo", "createdBy":"bar", "endsAt":"2020-02-13T13:00:02.084Z", "matchers": [ {"isRegex":false,"Name":"foo","Value":"bar"} ], "startsAt":"2020-02-13T12:02:01Z" }`, labelv: []string{"default"}, upstream: createSilenceWithLabel("default"), expCode: http.StatusOK, expBody: okResponse, }, { // Creation of a silence with an existing namespace label is ok. data: `{ "comment":"foo", "createdBy":"bar", "endsAt":"2020-02-13T13:00:02.084Z", "matchers": [ {"isRegex":false,"Name":"foo","Value":"bar"}, {"isRegex":false,"Name":"namespace","Value":"not default"} ], "startsAt":"2020-02-13T12:02:01Z" }`, labelv: []string{"default"}, upstream: createSilenceWithLabel("default"), expCode: http.StatusOK, expBody: okResponse, }, { // Creation of a silence without matcher returns an error. data: `{ "comment":"foo", "createdBy":"bar", "endsAt":"2020-02-13T13:00:02.084Z", "matchers": [], "startsAt":"2020-02-13T12:02:01Z" }`, labelv: []string{"default"}, expCode: http.StatusBadRequest, }, { // Update of an existing silence with a matching label is ok. data: `{ "id":"` + silID + `", "comment":"foo", "createdBy":"bar", "endsAt":"2020-02-13T13:00:02.084Z", "matchers": [ {"isRegex":false,"Name":"foo","Value":"bar"} ], "startsAt":"2020-02-13T12:02:01Z" }`, labelv: []string{"default"}, upstream: &chainedHandlers{ handlers: []http.Handler{ getSilenceWithLabel("default"), createSilenceWithLabel("default"), }, }, expCode: http.StatusOK, expBody: okResponse, }, { // Update of an existing silence with a non-matching label is denied. data: `{ "id":"` + silID + `", "comment":"foo", "createdBy":"bar", "endsAt":"2020-02-13T13:00:02.084Z", "matchers": [ {"isRegex":false,"Name":"foo","Value":"bar"} ], "startsAt":"2020-02-13T12:02:01Z" }`, labelv: []string{"default"}, upstream: &chainedHandlers{ handlers: []http.Handler{ getSilenceWithLabel("not default"), createSilenceWithLabel("default"), }, }, expCode: http.StatusForbidden, }, { // Update of a non-existing silence fails. data: `{ "id":"does not exist", "comment":"foo", "createdBy":"bar", "endsAt":"2020-02-13T13:00:02.084Z", "matchers": [ {"isRegex":false,"Name":"foo","Value":"bar"} ], "startsAt":"2020-02-13T12:02:01Z" }`, labelv: []string{"default"}, upstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { http.NotFound(w, req) }), expCode: http.StatusBadGateway, }, { // The silence has the expected value for the label but upstream returns an error. data: `{ "id":"` + silID + `", "comment":"foo", "createdBy":"bar", "endsAt":"2020-02-13T13:00:02.084Z", "matchers": [ {"isRegex":false,"Name":"foo","Value":"bar"} ], "startsAt":"2020-02-13T12:02:01Z" }`, labelv: []string{"default"}, upstream: &chainedHandlers{ handlers: []http.Handler{ getSilenceWithLabel("default"), http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusTeapot) }), }, }, expCode: http.StatusTeapot, }, { // Multiple label values are not supported. labelv: []string{"default", "something"}, expCode: http.StatusUnprocessableEntity, }, } { t.Run("", func(t *testing.T) { m := newMockUpstream(tc.upstream) defer m.Close() r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}) if err != nil { t.Fatalf("unexpected error: %v", err) } u, err := url.Parse("http://alertmanager.example.com/api/v2/silences/") if err != nil { t.Fatalf("unexpected error: %v", err) } q := u.Query() for _, s := range tc.labelv { q.Add(proxyLabel, s) } u.RawQuery = q.Encode() w := httptest.NewRecorder() req := httptest.NewRequest("POST", u.String(), bytes.NewBufferString(tc.data)) r.ServeHTTP(w, req) resp := w.Result() body, _ := io.ReadAll(resp.Body) defer resp.Body.Close() if resp.StatusCode != tc.expCode { t.Logf("expected status code %d, got %d", tc.expCode, resp.StatusCode) t.Logf("%s", string(body)) t.FailNow() } if resp.StatusCode != http.StatusOK { return } if string(body) != string(tc.expBody) { t.Fatalf("expected body %q, got %q", string(tc.expBody), string(body)) } }) } } func TestGetAlertGroups(t *testing.T) { for _, tc := range []struct { labelv []string filters []string expCode int expQueryValues []string queryParam string url string }{ { // No "namespace" parameter returns an error. expCode: http.StatusBadRequest, url: "http://alertmanager.example.com/api/v2/alerts/groups", }, { // Check for other query parameters labelv: []string{"default"}, expCode: http.StatusOK, expQueryValues: []string{"false"}, queryParam: "silenced", url: "http://alertmanager.example.com/api/v2/alerts/groups?silenced=false", }, { // Check for filter parameter. labelv: []string{"default"}, filters: []string{`job="prometheus"`, `instance=~".+"`}, expCode: http.StatusOK, expQueryValues: []string{`job="prometheus"`, `instance=~".+"`, `namespace="default"`}, queryParam: "filter", url: "http://alertmanager.example.com/api/v2/alerts/groups", }, { // Check for filter parameter with multiple label values. labelv: []string{"default", "something"}, filters: []string{`job="prometheus"`, `instance=~".+"`}, expCode: http.StatusOK, expQueryValues: []string{`job="prometheus"`, `instance=~".+"`, `namespace=~"default|something"`}, queryParam: "filter", url: "http://alertmanager.example.com/api/v2/alerts/groups", }, } { t.Run(strings.Join(tc.filters, "&"), func(t *testing.T) { m := newMockUpstream(checkQueryHandler("", tc.queryParam, tc.expQueryValues...)) defer m.Close() r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}) if err != nil { t.Fatalf("unexpected error: %v", err) } u, err := url.Parse(tc.url) if err != nil { t.Fatalf("unexpected error: %v", err) } q := u.Query() for _, m := range tc.filters { q.Add("filter", m) } for _, s := range tc.labelv { q.Add(proxyLabel, s) } u.RawQuery = q.Encode() w := httptest.NewRecorder() req := httptest.NewRequest("GET", u.String(), nil) r.ServeHTTP(w, req) resp := w.Result() body, _ := io.ReadAll(resp.Body) defer resp.Body.Close() if resp.StatusCode != tc.expCode { t.Logf("expected status code %d, got %d", tc.expCode, resp.StatusCode) t.Logf("%s", string(body)) t.FailNow() } if resp.StatusCode != http.StatusOK { return } }) } } ================================================ FILE: injectproxy/testdata/alerts_incomplete_upstream_response.golden ================================================ ================================================ FILE: injectproxy/testdata/alerts_invalid_upstream_response.golden ================================================ ================================================ FILE: injectproxy/testdata/alerts_match_namespace_ns1.golden ================================================ { "status": "success", "data": { "alerts": [ { "labels": { "alertname": "Alert1", "namespace": "ns1" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert2", "namespace": "ns1", "operation": "update" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert2", "namespace": "ns1", "operation": "delete" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" } ] } } ================================================ FILE: injectproxy/testdata/alerts_match_namespace_ns2.golden ================================================ { "status": "success", "data": { "alerts": [ { "labels": { "alertname": "Alert3", "namespace": "ns2" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:39.972915521+01:00", "value": "0e+00" } ] } } ================================================ FILE: injectproxy/testdata/alerts_match_namespaces_ns1_and_ns2.golden ================================================ { "status": "success", "data": { "alerts": [ { "labels": { "alertname": "Alert1", "namespace": "ns1" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert2", "namespace": "ns1", "operation": "update" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert2", "namespace": "ns1", "operation": "delete" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert3", "namespace": "ns2" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:39.972915521+01:00", "value": "0e+00" } ] } } ================================================ FILE: injectproxy/testdata/alerts_no_match.golden ================================================ { "status": "success", "data": { "alerts": [] } } ================================================ FILE: injectproxy/testdata/alerts_no_namespace_error.golden ================================================ {"error":"The \"namespace\" query parameter must be provided.","errorType":"prom-label-proxy","status":"error"} ================================================ FILE: injectproxy/testdata/alerts_upstream_error.golden ================================================ error ================================================ FILE: injectproxy/testdata/rules_incomplete_upstream_response.golden ================================================ ================================================ FILE: injectproxy/testdata/rules_invalid_upstream_response.golden ================================================ ================================================ FILE: injectproxy/testdata/rules_match_namespace_ns1.golden ================================================ { "status": "success", "data": { "groups": [ { "name": "group1", "file": "testdata/rules1.yml", "rules": [ { "name": "metric1", "query": "0", "labels": { "namespace": "ns1" }, "health": "ok", "evaluationTime": 0.000214303, "lastEvaluation": "2024-04-29T14:23:52.403557247+02:00", "type": "recording" }, { "name": "metric2", "query": "1", "labels": { "namespace": "ns1", "operation": "create" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:53.403557247+02:00", "type": "recording" }, { "name": "metric2", "query": "0", "labels": { "namespace": "ns1", "operation": "update" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:54.403557247+02:00", "type": "recording" }, { "name": "metric2", "query": "0", "labels": { "namespace": "ns1", "operation": "delete" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:53.603557247+02:00", "type": "recording" }, { "state": "firing", "name": "Alert1", "query": "metric1{namespace=\"ns1\"} == 0", "duration": 0, "keepFiringFor": 0, "labels": { "namespace": "ns1" }, "annotations": {}, "alerts": [ { "labels": { "alertname": "Alert1", "namespace": "ns1" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" } ], "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:53.803557247+02:00", "type": "alerting" }, { "state": "firing", "name": "Alert2", "query": "metric2{namespace=\"ns1\"} == 0", "duration": 0, "keepFiringFor": 0, "labels": { "namespace": "ns1" }, "annotations": {}, "alerts": [ { "labels": { "alertname": "Alert2", "namespace": "ns1", "operation": "update" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert2", "namespace": "ns1", "operation": "delete" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" } ], "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:53.903557247+02:00", "type": "alerting" } ], "interval": 10 } ] } } ================================================ FILE: injectproxy/testdata/rules_match_namespace_ns2.golden ================================================ { "status": "success", "data": { "groups": [ { "name": "group1", "file": "testdata/rules2.yml", "rules": [ { "name": "metric1", "query": "1", "labels": { "namespace": "ns2" }, "health": "ok", "evaluationTime": 0.000214303, "lastEvaluation": "2024-04-29T14:23:52.403557247+02:00", "type": "recording" }, { "state": "inactive", "name": "Alert1", "query": "metric1{namespace=\"ns2\"} == 0", "duration": 0, "keepFiringFor": 0, "labels": { "namespace": "ns2" }, "annotations": {}, "alerts": [], "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.503557247+02:00", "type": "alerting" } ], "interval": 10 }, { "name": "group2", "file": "testdata/rules2.yml", "rules": [ { "name": "metric2", "query": "1", "labels": { "namespace": "ns2", "operation": "create" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.503557247+02:00", "type": "recording" }, { "name": "metric2", "query": "2", "labels": { "namespace": "ns2", "operation": "update" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.603557247+02:00", "type": "recording" }, { "name": "metric2", "query": "3", "labels": { "namespace": "ns2", "operation": "delete" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.643557247+02:00", "type": "recording" }, { "name": "metric3", "query": "0", "labels": { "namespace": "ns2" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.683557247+02:00", "type": "recording" }, { "state": "inactive", "name": "Alert2", "query": "metric2{namespace=\"ns2\"} == 0", "duration": 0, "keepFiringFor": 0, "labels": { "namespace": "ns2" }, "annotations": {}, "alerts": [], "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.803557247+02:00", "type": "alerting" }, { "state": "firing", "name": "Alert3", "query": "metric3{namespace=\"ns2\"} == 0", "duration": 0, "keepFiringFor": 0, "labels": { "namespace": "ns2" }, "annotations": {}, "alerts": [ { "labels": { "alertname": "Alert3", "namespace": "ns2" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:39.972915521+01:00", "value": "0e+00" } ], "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.903557247+02:00", "type": "alerting" } ], "interval": 10 } ] } } ================================================ FILE: injectproxy/testdata/rules_match_namespaces_ns1_and_ns2.golden ================================================ { "status": "success", "data": { "groups": [ { "name": "group1", "file": "testdata/rules1.yml", "rules": [ { "name": "metric1", "query": "0", "labels": { "namespace": "ns1" }, "health": "ok", "evaluationTime": 0.000214303, "lastEvaluation": "2024-04-29T14:23:52.403557247+02:00", "type": "recording" }, { "name": "metric2", "query": "1", "labels": { "namespace": "ns1", "operation": "create" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:53.403557247+02:00", "type": "recording" }, { "name": "metric2", "query": "0", "labels": { "namespace": "ns1", "operation": "update" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:54.403557247+02:00", "type": "recording" }, { "name": "metric2", "query": "0", "labels": { "namespace": "ns1", "operation": "delete" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:53.603557247+02:00", "type": "recording" }, { "state": "firing", "name": "Alert1", "query": "metric1{namespace=\"ns1\"} == 0", "duration": 0, "keepFiringFor": 0, "labels": { "namespace": "ns1" }, "annotations": {}, "alerts": [ { "labels": { "alertname": "Alert1", "namespace": "ns1" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" } ], "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:53.803557247+02:00", "type": "alerting" }, { "state": "firing", "name": "Alert2", "query": "metric2{namespace=\"ns1\"} == 0", "duration": 0, "keepFiringFor": 0, "labels": { "namespace": "ns1" }, "annotations": {}, "alerts": [ { "labels": { "alertname": "Alert2", "namespace": "ns1", "operation": "update" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert2", "namespace": "ns1", "operation": "delete" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:44.543981127+01:00", "value": "0e+00" } ], "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:53.903557247+02:00", "type": "alerting" } ], "interval": 10 }, { "name": "group1", "file": "testdata/rules2.yml", "rules": [ { "name": "metric1", "query": "1", "labels": { "namespace": "ns2" }, "health": "ok", "evaluationTime": 0.000214303, "lastEvaluation": "2024-04-29T14:23:52.403557247+02:00", "type": "recording" }, { "state": "inactive", "name": "Alert1", "query": "metric1{namespace=\"ns2\"} == 0", "duration": 0, "keepFiringFor": 0, "labels": { "namespace": "ns2" }, "annotations": {}, "alerts": [], "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.503557247+02:00", "type": "alerting" } ], "interval": 10 }, { "name": "group2", "file": "testdata/rules2.yml", "rules": [ { "name": "metric2", "query": "1", "labels": { "namespace": "ns2", "operation": "create" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.503557247+02:00", "type": "recording" }, { "name": "metric2", "query": "2", "labels": { "namespace": "ns2", "operation": "update" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.603557247+02:00", "type": "recording" }, { "name": "metric2", "query": "3", "labels": { "namespace": "ns2", "operation": "delete" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.643557247+02:00", "type": "recording" }, { "name": "metric3", "query": "0", "labels": { "namespace": "ns2" }, "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.683557247+02:00", "type": "recording" }, { "state": "inactive", "name": "Alert2", "query": "metric2{namespace=\"ns2\"} == 0", "duration": 0, "keepFiringFor": 0, "labels": { "namespace": "ns2" }, "annotations": {}, "alerts": [], "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.803557247+02:00", "type": "alerting" }, { "state": "firing", "name": "Alert3", "query": "metric3{namespace=\"ns2\"} == 0", "duration": 0, "keepFiringFor": 0, "labels": { "namespace": "ns2" }, "annotations": {}, "alerts": [ { "labels": { "alertname": "Alert3", "namespace": "ns2" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:39.972915521+01:00", "value": "0e+00" } ], "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.903557247+02:00", "type": "alerting" } ], "interval": 10 } ] } } ================================================ FILE: injectproxy/testdata/rules_no_match.golden ================================================ { "status": "success", "data": { "groups": [] } } ================================================ FILE: injectproxy/testdata/rules_no_match_with_gzip_not_requested.golden ================================================ { "status": "success", "data": { "groups": [] } } ================================================ FILE: injectproxy/testdata/rules_no_match_with_gzip_requested.golden ================================================ { "status": "success", "data": { "groups": [] } } ================================================ FILE: injectproxy/testdata/rules_no_namespace_error.golden ================================================ {"error":"The \"namespace\" query parameter must be provided.","errorType":"prom-label-proxy","status":"error"} ================================================ FILE: injectproxy/testdata/rules_upstream_error.golden ================================================ error ================================================ FILE: injectproxy/testdata/rules_with_active_alerts.golden ================================================ { "status": "success", "data": { "groups": [ { "name": "group3", "file": "testdata/rules3.yml", "rules": [ { "state": "pending", "name": "Alert3", "query": "metric4{ns!=\"default\"} == 0", "duration": 300, "keepFiringFor": 0, "labels": {}, "annotations": {}, "alerts": [ { "labels": { "alertname": "Alert3", "namespace": "ns3" }, "annotations": {}, "state": "pending", "activeAt": "2019-12-18T13:20:39.972915521+01:00", "value": "0e+00" } ], "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.903557247+02:00", "type": "alerting" }, { "state": "firing", "name": "Alert4", "query": "metric5 == 0", "duration": 300, "keepFiringFor": 0, "labels": {}, "annotations": {}, "alerts": [ { "labels": { "alertname": "Alert4", "namespace": "ns3", "state": "foo" }, "annotations": {}, "state": "pending", "activeAt": "2019-12-18T13:20:39.972915521+01:00", "value": "0e+00" }, { "labels": { "alertname": "Alert1", "namespace": "ns3", "state": "bar" }, "annotations": {}, "state": "firing", "activeAt": "2019-12-18T13:14:39.972915521+01:00", "value": "0e+00" } ], "health": "ok", "evaluationTime": 0.000214, "lastEvaluation": "2024-04-29T14:23:52.903557247+02:00", "type": "alerting" } ], "interval": 10 } ] } } ================================================ FILE: injectproxy/testdata/rules_with_label_matchers.golden ================================================ { "status": "success", "data": { "groups": [ { "name": "group1", "file": "testdata/rules1.yml", "rules": [ { "name": "metric1", "query": "0", "labels": { "namespace": "ns1" }, "health": "ok", "type": "recording", "evaluationTime": 0.000214303, "lastEvaluation": "2024-04-29T14:23:52.403557247+02:00" } ], "interval": 10 } ] } } ================================================ FILE: injectproxy/utils.go ================================================ // Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package injectproxy import ( "encoding/json" "log" "net/http" ) func prometheusAPIError(w http.ResponseWriter, errorMessage string, code int) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(code) res := map[string]string{"status": "error", "errorType": "prom-label-proxy", "error": errorMessage} if err := json.NewEncoder(w).Encode(res); err != nil { log.Printf("error: Failed to encode json: %v", err) } } ================================================ FILE: main.go ================================================ // Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "errors" "flag" "fmt" "log" "net" "net/http" "net/url" "os" "regexp" "strings" "syscall" "github.com/metalmatze/signal/internalserver" "github.com/oklog/run" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus-community/prom-label-proxy/injectproxy" ) type arrayFlags []string // String is the method to format the flag's value, part of the flag.Value interface. // The String method's output will be used in diagnostics. func (i *arrayFlags) String() string { return fmt.Sprint(*i) } // Set is the method to set the flag value, part of the flag.Value interface. func (i *arrayFlags) Set(value string) error { if value == "" { return nil } *i = append(*i, value) return nil } func main() { var ( insecureListenAddress string internalListenAddress string upstream string upstreamCaCert string queryParam string headerName string label string labelValues arrayFlags enableLabelAPIs bool unsafePassthroughPaths string // Comma-delimited string. insecureSkipVerify bool errorOnReplace bool regexMatch bool headerUsesListSyntax bool rulesWithActiveAlerts bool labelMatchersForRulesAPI bool promQLDurationExpressionParsing bool promQLExperimentalFunctions bool promQLExtendedRangeSelectors bool promQLBinopFillModifiers bool ) flagset := flag.NewFlagSet(os.Args[0], flag.ExitOnError) flagset.StringVar(&insecureListenAddress, "insecure-listen-address", "", "The address the prom-label-proxy HTTP server should listen on.") flagset.StringVar(&internalListenAddress, "internal-listen-address", "", "The address the internal prom-label-proxy HTTP server should listen on to expose metrics about itself.") flagset.StringVar(&queryParam, "query-param", "", "Name of the HTTP parameter that contains the tenant value. At most one of -query-param, -header-name and -label-value should be given. If the flag isn't defined and neither -header-name nor -label-value is set, it will default to the value of the -label flag.") flagset.StringVar(&headerName, "header-name", "", "Name of the HTTP header name that contains the tenant value. At most one of -query-param, -header-name and -label-value should be given.") flagset.StringVar(&upstream, "upstream", "", "The upstream URL to proxy to.") flagset.StringVar(&upstreamCaCert, "upstream-ca-cert", "", "The upstream ca certificate file.") flagset.StringVar(&label, "label", "", "The label name to enforce in all proxied PromQL queries.") flagset.Var(&labelValues, "label-value", "A fixed label value to enforce in all proxied PromQL queries. At most one of -query-param, -header-name and -label-value should be given. It can be repeated in which case the proxy will enforce the union of values.") flagset.BoolVar(&enableLabelAPIs, "enable-label-apis", false, "When specified proxy allows to inject label to label APIs like /api/v1/labels and /api/v1/label//values. "+ "NOTE: Enable with care because filtering by matcher is not implemented in older versions of Prometheus (>= v2.24.0 required) and Thanos (>= v0.18.0 required, >= v0.23.0 recommended). If enabled and "+ "any labels endpoint does not support selectors, the injected matcher will have no effect.") flagset.StringVar(&unsafePassthroughPaths, "unsafe-passthrough-paths", "", "Comma delimited allow list of exact HTTP path segments that should be allowed to hit upstream URL without any enforcement. "+ "This option is checked after Prometheus APIs, you cannot override enforced API endpoints to be not enforced with this option. Use carefully as it can easily cause a data leak if the provided path is an important "+ "API (like /api/v1/configuration) which isn't enforced by prom-label-proxy. NOTE: \"all\" matching paths like \"/\" or \"\" and regex are not allowed.") flagset.BoolVar(&insecureSkipVerify, "insecure-skip-verify", false, "When specified, the proxy will bypass validation of the server's TLS/SSL certificate.") flagset.BoolVar(&errorOnReplace, "error-on-replace", false, "When specified, the proxy will return HTTP status code 400 if the query already contains a label matcher that differs from the one the proxy would inject.") flagset.BoolVar(®exMatch, "regex-match", false, "When specified, the tenant name is treated as a regular expression. In this case, only one tenant name should be provided.") flagset.BoolVar(&headerUsesListSyntax, "header-uses-list-syntax", false, "When specified, the header line value will be parsed as a comma-separated list. This allows a single tenant header line to specify multiple tenant names.") flagset.BoolVar(&rulesWithActiveAlerts, "rules-with-active-alerts", false, "When true, the proxy will return alerting rules with active alerts matching the tenant label even when the tenant label isn't present in the rule's labels.") flagset.BoolVar(&labelMatchersForRulesAPI, "enable-label-matchers-for-rules-api", false, "When true, the proxy uses label matchers when querying the /api/v1/rules endpoint. NOTE: Enable with care because filtering by label matcher is not implemented in older versions of Prometheus (>= 2.54.0 required) and Thanos (>= v0.25.0 required). If not implemented by upstream, the response will not be filtered accordingly.") flagset.BoolVar(&promQLDurationExpressionParsing, "enable-promql-duration-expression-parsing", false, "When true, the proxy supports arithmetic for durations in PromQL expressions.") flagset.BoolVar(&promQLExperimentalFunctions, "enable-promql-experimental-functions", false, "When true, the proxy supports experimental functions in PromQL expressions.") flagset.BoolVar(&promQLExtendedRangeSelectors, "enable-promql-extended-range-selectors", false, "When true, the proxy supports extended range selectors in PromQL expressions.") flagset.BoolVar(&promQLBinopFillModifiers, "enable-promql-binop-fill-modifiers", false, "When true, the proxy supports binary operation fill modifiers in PromQL expressions.") //nolint: errcheck // Parse() will exit on error. flagset.Parse(os.Args[1:]) if label == "" { log.Fatalf("-label flag cannot be empty") } if len(labelValues) == 0 && queryParam == "" && headerName == "" { queryParam = label } if len(labelValues) > 0 { if queryParam != "" || headerName != "" { log.Fatalf("at most one of -query-param, -header-name and -label-value must be set") } } else if queryParam != "" && headerName != "" { log.Fatalf("at most one of -query-param, -header-name and -label-value must be set") } upstreamURL, err := url.Parse(upstream) if err != nil { log.Fatalf("Failed to build parse upstream URL: %v", err) } if upstreamURL.Scheme != "http" && upstreamURL.Scheme != "https" { log.Fatalf("Invalid scheme for upstream URL %q, only 'http' and 'https' are supported", upstream) } reg := prometheus.NewRegistry() reg.MustRegister( collectors.NewGoCollector(), collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), ) opts := []injectproxy.Option{injectproxy.WithPrometheusRegistry(reg)} if upstreamCaCert != "" { opts = append(opts, injectproxy.WithUpstreamCaCert(upstreamCaCert)) } if enableLabelAPIs { opts = append(opts, injectproxy.WithEnabledLabelsAPI()) } if len(unsafePassthroughPaths) > 0 { opts = append(opts, injectproxy.WithPassthroughPaths(strings.Split(unsafePassthroughPaths, ","))) } if insecureSkipVerify { opts = append(opts, injectproxy.WithInsecureSkipVerify()) } if errorOnReplace { opts = append(opts, injectproxy.WithErrorOnReplace()) } if rulesWithActiveAlerts { opts = append(opts, injectproxy.WithActiveAlerts()) } if labelMatchersForRulesAPI { opts = append(opts, injectproxy.WithLabelMatchersForRulesAPI()) } if regexMatch { if len(labelValues) > 0 { if len(labelValues) > 1 { log.Fatalf("Regex match is limited to one label value") } compiledRegex, err := regexp.Compile(labelValues[0]) if err != nil { log.Fatalf("Invalid regexp: %v", err.Error()) return } if compiledRegex.MatchString("") { log.Fatalf("Regex should not match empty string") return } } opts = append(opts, injectproxy.WithRegexMatch()) } if promQLDurationExpressionParsing { opts = append(opts, injectproxy.WithPromqlDurationExpressionParsing()) } if promQLExperimentalFunctions { opts = append(opts, injectproxy.WithPromqlExperimentalFunctions()) } if promQLExtendedRangeSelectors { opts = append(opts, injectproxy.WithPromqlExtendedRangeSelectors()) } if promQLBinopFillModifiers { opts = append(opts, injectproxy.WithPromqlBinopFillModifiers()) } var extractLabeler injectproxy.ExtractLabeler switch { case len(labelValues) > 0: extractLabeler = injectproxy.StaticLabelEnforcer(labelValues) case queryParam != "": extractLabeler = injectproxy.HTTPFormEnforcer{ParameterName: queryParam} case headerName != "": extractLabeler = injectproxy.HTTPHeaderEnforcer{Name: http.CanonicalHeaderKey(headerName), ParseListSyntax: headerUsesListSyntax} } var g run.Group { // Run the insecure HTTP server. routes, err := injectproxy.NewRoutes(upstreamURL, label, extractLabeler, opts...) if err != nil { log.Fatalf("Failed to create injectproxy Routes: %v", err) } mux := http.NewServeMux() mux.Handle("/", routes) l, err := net.Listen("tcp", insecureListenAddress) if err != nil { log.Fatalf("Failed to listen on insecure address: %v", err) } srv := &http.Server{Handler: mux} g.Add(func() error { log.Printf("Listening insecurely on %v", l.Addr()) if err := srv.Serve(l); err != nil && err != http.ErrServerClosed { log.Printf("Server stopped with %v", err) return err } return nil }, func(error) { srv.Close() }) } if internalListenAddress != "" { // Run the internal HTTP server. h := internalserver.NewHandler( internalserver.WithName("Internal prom-label-proxy API"), internalserver.WithPrometheusRegistry(reg), internalserver.WithPProf(), ) // Run the HTTP server. l, err := net.Listen("tcp", internalListenAddress) if err != nil { log.Fatalf("Failed to listen on internal address: %v", err) } srv := &http.Server{Handler: h} g.Add(func() error { log.Printf("Listening on %v for metrics and pprof", l.Addr()) if err := srv.Serve(l); err != nil && err != http.ErrServerClosed { log.Printf("Internal server stopped with %v", err) return err } return nil }, func(error) { srv.Close() }) } g.Add(run.SignalHandler(context.Background(), syscall.SIGINT, syscall.SIGTERM)) if err := g.Run(); err != nil { if !errors.As(err, &run.SignalError{}) { log.Printf("Server stopped with %v", err) os.Exit(1) } log.Print("Caught signal; exiting gracefully...") } }