Showing preview only (294K chars total). Download the full file or copy to clipboard to get everything.
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/<name>/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 <prometheus-developers@googlegroups.com>"
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
[](https://github.com/prometheus-community/prom-label-proxy/actions/workflows/ci.yml)
[](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/<name>/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/<name>/values` the proxy injects the specified label all the provided `match[]` selectors.
NOTE: When the `/api/v1/labels` and `/api/v1/label/<name>/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 <nil>, 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/<label_name>/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/<name>/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) (*apiRe
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
SYMBOL INDEX (138 symbols across 12 files)
FILE: injectproxy/alerts.go
method alerts (line 19) | func (r *routes) alerts(w http.ResponseWriter, req *http.Request) {
FILE: injectproxy/alerts_test.go
function TestGetAlerts (line 25) | func TestGetAlerts(t *testing.T) {
FILE: injectproxy/enforce.go
type PromQLEnforcer (line 26) | type PromQLEnforcer struct
method Enforce (line 62) | func (ms *PromQLEnforcer) Enforce(q string) (string, error) {
method EnforceNode (line 87) | func (ms PromQLEnforcer) EnforceNode(node parser.Node) error {
method EnforceMatchers (line 176) | func (ms PromQLEnforcer) EnforceMatchers(targets []*labels.Matcher) ([...
function NewPromQLEnforcer (line 32) | func NewPromQLEnforcer(errorOnReplace bool, ms ...*labels.Matcher) *Prom...
function NewPromQLEnforcerWithOptions (line 36) | func NewPromQLEnforcerWithOptions(errorOnReplace bool, parserOptions par...
function defaultParserOptions (line 305) | func defaultParserOptions() parser.Options {
FILE: injectproxy/enforce_test.go
function mustNewMatcher (line 25) | func mustNewMatcher(t labels.MatchType, n, v string) *labels.Matcher {
type checkFunc (line 34) | type checkFunc
function checks (line 36) | func checks(cs ...checkFunc) checkFunc {
function noError (line 47) | func noError() checkFunc {
function errorIs (line 57) | func errorIs(want error) checkFunc {
function hasExpression (line 66) | func hasExpression(want string) checkFunc {
function TestEnforce (line 360) | func TestEnforce(t *testing.T) {
function TestEnforceWithErrOnReplace (line 371) | func TestEnforceWithErrOnReplace(t *testing.T) {
function TestEnforceWithParserOptions (line 999) | func TestEnforceWithParserOptions(t *testing.T) {
FILE: injectproxy/routes.go
constant queryParam (line 42) | queryParam = "query"
constant matchersParam (line 43) | matchersParam = "match[]"
type routes (line 46) | type routes struct
method ServeHTTP (line 490) | func (r *routes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
method ModifyResponse (line 494) | func (r *routes) ModifyResponse(resp *http.Response) error {
method errorHandler (line 504) | func (r *routes) errorHandler(rw http.ResponseWriter, _ *http.Request,...
method errorIfRegexpMatch (line 523) | func (r *routes) errorIfRegexpMatch(next http.HandlerFunc) http.Handle...
method passthrough (line 578) | func (r *routes) passthrough(w http.ResponseWriter, req *http.Request) {
method query (line 582) | func (r *routes) query(w http.ResponseWriter, req *http.Request) {
method newLabelMatcher (line 663) | func (r *routes) newLabelMatcher(vals ...string) (*labels.Matcher, err...
method matcher (line 699) | func (r *routes) matcher(w http.ResponseWriter, req *http.Request) {
method injectMatcher (line 733) | func (r *routes) injectMatcher(q url.Values, matcher *labels.Matcher) ...
type options (line 62) | type options struct
type Option (line 75) | type Option interface
type optionFunc (line 79) | type optionFunc
method apply (line 81) | func (f optionFunc) apply(o *options) {
function WithPrometheusRegistry (line 86) | func WithPrometheusRegistry(reg prometheus.Registerer) Option {
function WithUpstreamCaCert (line 93) | func WithUpstreamCaCert(caCert string) Option {
function WithEnabledLabelsAPI (line 100) | func WithEnabledLabelsAPI() Option {
function WithPassthroughPaths (line 109) | func WithPassthroughPaths(paths []string) Option {
function WithInsecureSkipVerify (line 116) | func WithInsecureSkipVerify() Option {
function WithErrorOnReplace (line 124) | func WithErrorOnReplace() Option {
function WithActiveAlerts (line 131) | func WithActiveAlerts() Option {
function WithLabelMatchersForRulesAPI (line 138) | func WithLabelMatchersForRulesAPI() Option {
function WithRegexMatch (line 145) | func WithRegexMatch() Option {
function WithPromqlDurationExpressionParsing (line 152) | func WithPromqlDurationExpressionParsing() Option {
function WithPromqlExperimentalFunctions (line 159) | func WithPromqlExperimentalFunctions() Option {
function WithPromqlExtendedRangeSelectors (line 166) | func WithPromqlExtendedRangeSelectors() Option {
function WithPromqlBinopFillModifiers (line 173) | func WithPromqlBinopFillModifiers() Option {
type mux (line 180) | type mux interface
type strictMux (line 186) | type strictMux struct
method Handle (line 205) | func (s *strictMux) Handle(pattern string, handler http.Handler) error {
function newStrictMux (line 191) | func newStrictMux(m mux) *strictMux {
type instrumentedMux (line 228) | type instrumentedMux struct
method Handle (line 241) | func (i *instrumentedMux) Handle(pattern string, handler http.Handler) {
function newInstrumentedMux (line 233) | func newInstrumentedMux(m mux, r prometheus.Registerer) *instrumentedMux {
type ExtractLabeler (line 249) | type ExtractLabeler interface
type HTTPFormEnforcer (line 254) | type HTTPFormEnforcer struct
method ExtractLabel (line 259) | func (hff HTTPFormEnforcer) ExtractLabel(next http.HandlerFunc) http.H...
method getLabelValues (line 292) | func (hff HTTPFormEnforcer) getLabelValues(r *http.Request) ([]string,...
type HTTPHeaderEnforcer (line 307) | type HTTPHeaderEnforcer struct
method ExtractLabel (line 313) | func (hhe HTTPHeaderEnforcer) ExtractLabel(next http.HandlerFunc) http...
method getLabelValues (line 325) | func (hhe HTTPHeaderEnforcer) getLabelValues(r *http.Request) ([]strin...
type StaticLabelEnforcer (line 342) | type StaticLabelEnforcer
method ExtractLabel (line 345) | func (sle StaticLabelEnforcer) ExtractLabel(next http.HandlerFunc) htt...
function NewRoutes (line 351) | func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLa...
function enforceMethods (line 513) | func enforceMethods(h http.HandlerFunc, methods ...string) http.HandlerF...
type ctxKey (line 534) | type ctxKey
constant keyLabel (line 536) | keyLabel ctxKey = iota
function MustLabelValues (line 541) | func MustLabelValues(ctx context.Context) []string {
function MustLabelValue (line 559) | func MustLabelValue(ctx context.Context) string {
function labelValuesToRegexpString (line 564) | func labelValuesToRegexpString(labelValues []string) string {
function WithLabelValues (line 574) | func WithLabelValues(ctx context.Context, labels []string) context.Conte...
function enforceQueryValues (line 645) | func enforceQueryValues(e *PromQLEnforcer, v url.Values) (values string,...
function matchersToString (line 755) | func matchersToString(ms ...*labels.Matcher) string {
function humanFriendlyErrorMessage (line 765) | func humanFriendlyErrorMessage(err error) string {
function splitValues (line 773) | func splitValues(slice []string, sep string) []string {
function removeEmptyValues (line 785) | func removeEmptyValues(slice []string) []string {
function trimValues (line 796) | func trimValues(slice []string) []string {
FILE: injectproxy/routes_test.go
function checkParameterAbsent (line 30) | func checkParameterAbsent(param string, next http.Handler) http.Handler {
function checkFormParameterAbsent (line 46) | func checkFormParameterAbsent(param string, next http.Handler) http.Hand...
function checkQueryHandler (line 63) | func checkQueryHandler(body, key string, values ...string) http.Handler {
function checkFormHandler (line 103) | func checkFormHandler(key string, values ...string) http.Handler {
type mockUpstream (line 130) | type mockUpstream struct
method ServeHTTP (line 150) | func (m *mockUpstream) ServeHTTP(w http.ResponseWriter, req *http.Requ...
method Close (line 154) | func (m *mockUpstream) Close() {
function newMockUpstream (line 136) | func newMockUpstream(h http.Handler) *mockUpstream {
constant proxyLabel (line 158) | proxyLabel = "namespace"
function TestWithPassthroughPaths (line 160) | func TestWithPassthroughPaths(t *testing.T) {
function TestMatch (line 264) | func TestMatch(t *testing.T) {
function TestMatchWithPost (line 441) | func TestMatchWithPost(t *testing.T) {
function TestSeries (line 572) | func TestSeries(t *testing.T) {
function TestSeriesWithPost (line 702) | func TestSeriesWithPost(t *testing.T) {
function TestQuery (line 843) | func TestQuery(t *testing.T) {
FILE: injectproxy/rules.go
type apiResponse (line 29) | type apiResponse struct
function getAPIResponse (line 37) | func getAPIResponse(resp *http.Response) (*apiResponse, error) {
type rulesData (line 69) | type rulesData struct
type ruleGroup (line 73) | type ruleGroup struct
type rule (line 80) | type rule struct
method Labels (line 85) | func (r *rule) Labels() labels.Labels {
method MarshalJSON (line 93) | func (r *rule) MarshalJSON() ([]byte, error) {
method UnmarshalJSON (line 101) | func (r *rule) UnmarshalJSON(b []byte) error {
type alertingRule (line 128) | type alertingRule struct
type recordingRule (line 145) | type recordingRule struct
type alertsData (line 157) | type alertsData struct
type alert (line 161) | type alert struct
function modifyAPIResponse (line 177) | func modifyAPIResponse(f func([]string, *http.Request, *apiResponse) (an...
method filterRules (line 212) | func (r *routes) filterRules(lvalues []string, req *http.Request, resp *...
method filterAlerts (line 283) | func (r *routes) filterAlerts(lvalues []string, _ *http.Request, resp *a...
FILE: injectproxy/rules_test.go
type gzipResponseWriter (line 30) | type gzipResponseWriter struct
method Write (line 35) | func (w *gzipResponseWriter) Write(b []byte) (int, error) {
function gzipHandler (line 39) | func gzipHandler(next http.Handler) http.Handler {
function validRulesWithLabelMatchers (line 50) | func validRulesWithLabelMatchers(exp ...string) http.Handler {
function validRules (line 94) | func validRules() http.Handler {
function validAlerts (line 430) | func validAlerts() http.Handler {
function TestRules (line 485) | func TestRules(t *testing.T) {
function TestAlerts (line 674) | func TestAlerts(t *testing.T) {
function normalizeAPIResponse (line 814) | func normalizeAPIResponse(t *testing.T, b []byte) string {
FILE: injectproxy/silences.go
method silences (line 37) | func (r *routes) silences(w http.ResponseWriter, req *http.Request) {
function assertSingleLabelValue (line 50) | func assertSingleLabelValue(next http.HandlerFunc) http.HandlerFunc {
method enforceFilterParameter (line 64) | func (r *routes) enforceFilterParameter(w http.ResponseWriter, req *http...
method postSilence (line 122) | func (r *routes) postSilence(w http.ResponseWriter, req *http.Request) {
method deleteSilence (line 181) | func (r *routes) deleteSilence(w http.ResponseWriter, req *http.Request) {
method getSilenceByID (line 204) | func (r *routes) getSilenceByID(ctx context.Context, id string) (*models...
function hasMatcherForLabel (line 218) | func hasMatcherForLabel(matchers models.Matchers, name, value string) bo...
FILE: injectproxy/silences_test.go
function TestListSilences (line 30) | func TestListSilences(t *testing.T) {
constant silID (line 135) | silID = "802146e0-1f7a-42a6-ab0e-1e631479970b"
function getSilenceWithoutLabel (line 137) | func getSilenceWithoutLabel() http.Handler {
function getSilenceWithLabel (line 171) | func getSilenceWithLabel(labelv string) http.Handler {
function createSilenceWithLabel (line 205) | func createSilenceWithLabel(labelv string) http.Handler {
type chainedHandlers (line 232) | type chainedHandlers struct
method ServeHTTP (line 237) | func (c *chainedHandlers) ServeHTTP(w http.ResponseWriter, req *http.R...
function TestDeleteSilence (line 247) | func TestDeleteSilence(t *testing.T) {
function TestUpdateSilence (line 377) | func TestUpdateSilence(t *testing.T) {
function TestGetAlertGroups (line 578) | func TestGetAlertGroups(t *testing.T) {
FILE: injectproxy/utils.go
function prometheusAPIError (line 22) | func prometheusAPIError(w http.ResponseWriter, errorMessage string, code...
FILE: main.go
type arrayFlags (line 38) | type arrayFlags
method String (line 42) | func (i *arrayFlags) String() string {
method Set (line 47) | func (i *arrayFlags) Set(value string) error {
function main (line 56) | func main() {
Condensed preview — 62 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (315K chars).
[
{
"path": ".dockerignore",
"chars": 156,
"preview": "data/\n.build/\n.tarballs/\n\n!.build/linux-amd64/\n!.build/linux-arm64/\n!.build/linux-armv7/\n!.build/linux-ppc64le/\n!.build/"
},
{
"path": ".github/dependabot.yml",
"chars": 390,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"gomod\"\n directory: \"/\"\n schedule:\n interval: \"monthly\"\n - packag"
},
{
"path": ".github/workflows/ci.yml",
"chars": 2373,
"preview": "---\nname: CI\non:\n pull_request:\n push:\n branches: [main, master, 'release-*']\n tags: ['v*']\n\npermissions:\n cont"
},
{
"path": ".github/workflows/container_description.yml",
"chars": 2461,
"preview": "---\nname: Push README to Docker Hub\non:\n push:\n paths:\n - \"README.md\"\n - \"README-containers.md\"\n - \"."
},
{
"path": ".github/workflows/golangci-lint.yml",
"chars": 1562,
"preview": "---\n# This action is synced from https://github.com/prometheus/prometheus\nname: golangci-lint\non:\n push:\n branches: "
},
{
"path": ".github/workflows/govulncheck.yml",
"chars": 371,
"preview": "---\nname: govulncheck\non:\n pull_request:\n push:\n branches:\n - main\n - master\n schedule:\n - cron: '33 "
},
{
"path": ".gitignore",
"chars": 16,
"preview": "prom-label-proxy"
},
{
"path": ".golangci.yml",
"chars": 409,
"preview": "version: \"2\"\nlinters:\n exclusions:\n generated: lax\n presets:\n - comments\n - common-false-positives\n "
},
{
"path": ".promu.yml",
"chars": 832,
"preview": "---\ngo:\n # This must match .github/workflows/ci.yml.\n version: 1.26\nrepository:\n path: github.com/prometheus-co"
},
{
"path": ".yamllint",
"chars": 507,
"preview": "---\nextends: default\nignore: |\n **/node_modules\n web/api/v1/testdata/openapi_*_golden.yaml\n\nrules:\n braces:\n max-s"
},
{
"path": "CHANGELOG.md",
"chars": 3508,
"preview": "## 0.13.0 / 2026-06-21\n\n* [FEATURE] Add the `-insecure-skip-verify` flag to bypass the TLS verification of the upstream "
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 152,
"preview": "# Prometheus Community Code of Conduct\n\nPrometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation"
},
{
"path": "Dockerfile",
"chars": 325,
"preview": "ARG ARCH=\"amd64\"\nARG OS=\"linux\"\nFROM quay.io/prometheus/busybox-${OS}-${ARCH}:glibc\nLABEL maintainer=\"The Prometheus Aut"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "MAINTAINERS.md",
"chars": 126,
"preview": "# Maintainers\n\n* Lucas Servén Marín (lserven@gmail.com / @squat)\n* Simon Pasquier (pasquier.simon@gmail.com / @simonpasq"
},
{
"path": "Makefile",
"chars": 1162,
"preview": "# Copyright 2020 The Prometheus Authors\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not "
},
{
"path": "Makefile.common",
"chars": 16787,
"preview": "# Copyright The Prometheus Authors\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use t"
},
{
"path": "OWNERS",
"chars": 258,
"preview": "component: \"Monitoring\"\n\nreviewers:\n- brancz\n- krasi-georgiev\n- metalmatze\n- paulfantom\n- pgier\n- s-urbaniak\n- simonpasq"
},
{
"path": "README.md",
"chars": 11472,
"preview": "# prom-label-proxy\n\n[;\n// you may n"
},
{
"path": "injectproxy/alerts_test.go",
"chars": 4244,
"preview": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may n"
},
{
"path": "injectproxy/enforce.go",
"chars": 8668,
"preview": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may n"
},
{
"path": "injectproxy/enforce_test.go",
"chars": 21809,
"preview": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may n"
},
{
"path": "injectproxy/routes.go",
"chars": 23542,
"preview": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may n"
},
{
"path": "injectproxy/routes_test.go",
"chars": 43897,
"preview": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may n"
},
{
"path": "injectproxy/rules.go",
"chars": 8472,
"preview": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may n"
},
{
"path": "injectproxy/rules_test.go",
"chars": 22303,
"preview": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may n"
},
{
"path": "injectproxy/silences.go",
"chars": 6564,
"preview": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may n"
},
{
"path": "injectproxy/silences_test.go",
"chars": 17518,
"preview": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may n"
},
{
"path": "injectproxy/testdata/alerts_incomplete_upstream_response.golden",
"chars": 0,
"preview": ""
},
{
"path": "injectproxy/testdata/alerts_invalid_upstream_response.golden",
"chars": 0,
"preview": ""
},
{
"path": "injectproxy/testdata/alerts_match_namespace_ns1.golden",
"chars": 873,
"preview": "{\n \"status\": \"success\",\n \"data\": {\n \"alerts\": [\n {\n \"labels\": {\n \"alertname\": \"Alert1\",\n "
},
{
"path": "injectproxy/testdata/alerts_match_namespace_ns2.golden",
"chars": 311,
"preview": "{\n \"status\": \"success\",\n \"data\": {\n \"alerts\": [\n {\n \"labels\": {\n \"alertname\": \"Alert3\",\n "
},
{
"path": "injectproxy/testdata/alerts_match_namespaces_ns1_and_ns2.golden",
"chars": 1121,
"preview": "{\n \"status\": \"success\",\n \"data\": {\n \"alerts\": [\n {\n \"labels\": {\n \"alertname\": \"Alert1\",\n "
},
{
"path": "injectproxy/testdata/alerts_no_match.golden",
"chars": 59,
"preview": "{\n \"status\": \"success\",\n \"data\": {\n \"alerts\": []\n }\n}"
},
{
"path": "injectproxy/testdata/alerts_no_namespace_error.golden",
"chars": 112,
"preview": "{\"error\":\"The \\\"namespace\\\" query parameter must be provided.\",\"errorType\":\"prom-label-proxy\",\"status\":\"error\"}\n"
},
{
"path": "injectproxy/testdata/alerts_upstream_error.golden",
"chars": 5,
"preview": "error"
},
{
"path": "injectproxy/testdata/rules_incomplete_upstream_response.golden",
"chars": 0,
"preview": ""
},
{
"path": "injectproxy/testdata/rules_invalid_upstream_response.golden",
"chars": 0,
"preview": ""
},
{
"path": "injectproxy/testdata/rules_match_namespace_ns1.golden",
"chars": 3689,
"preview": "{\n \"status\": \"success\",\n \"data\": {\n \"groups\": [\n {\n \"name\": \"group1\",\n \"file\": \"testdata/rules1."
},
{
"path": "injectproxy/testdata/rules_match_namespace_ns2.golden",
"chars": 3898,
"preview": "{\n \"status\": \"success\",\n \"data\": {\n \"groups\": [\n {\n \"name\": \"group1\",\n \"file\": \"testdata/rules2."
},
{
"path": "injectproxy/testdata/rules_match_namespaces_ns1_and_ns2.golden",
"chars": 7524,
"preview": "{\n \"status\": \"success\",\n \"data\": {\n \"groups\": [\n {\n \"name\": \"group1\",\n \"file\": \"testdata/rules1."
},
{
"path": "injectproxy/testdata/rules_no_match.golden",
"chars": 59,
"preview": "{\n \"status\": \"success\",\n \"data\": {\n \"groups\": []\n }\n}"
},
{
"path": "injectproxy/testdata/rules_no_match_with_gzip_not_requested.golden",
"chars": 59,
"preview": "{\n \"status\": \"success\",\n \"data\": {\n \"groups\": []\n }\n}"
},
{
"path": "injectproxy/testdata/rules_no_match_with_gzip_requested.golden",
"chars": 59,
"preview": "{\n \"status\": \"success\",\n \"data\": {\n \"groups\": []\n }\n}"
},
{
"path": "injectproxy/testdata/rules_no_namespace_error.golden",
"chars": 112,
"preview": "{\"error\":\"The \\\"namespace\\\" query parameter must be provided.\",\"errorType\":\"prom-label-proxy\",\"status\":\"error\"}\n"
},
{
"path": "injectproxy/testdata/rules_upstream_error.golden",
"chars": 5,
"preview": "error"
},
{
"path": "injectproxy/testdata/rules_with_active_alerts.golden",
"chars": 2163,
"preview": "{\n \"status\": \"success\",\n \"data\": {\n \"groups\": [\n {\n \"name\": \"group3\",\n \"file\": \"testdata/rules3."
},
{
"path": "injectproxy/testdata/rules_with_label_matchers.golden",
"chars": 523,
"preview": "{\n \"status\": \"success\",\n \"data\": {\n \"groups\": [\n {\n \"name\": \"group1\",\n \"file\": \"testdata/rules1."
},
{
"path": "injectproxy/utils.go",
"chars": 1101,
"preview": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may n"
},
{
"path": "main.go",
"chars": 11671,
"preview": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may n"
}
]
About this extraction
This page contains the full source code of the prometheus-community/prom-label-proxy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 62 files (275.8 KB), approximately 88.1k tokens, and a symbol index with 138 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.