main bab0fc7ea168 cached
62 files
275.8 KB
88.1k tokens
138 symbols
1 requests
Download .txt
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

[![Build Status](https://github.com/prometheus-community/prom-label-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/prometheus-community/prom-label-proxy/actions/workflows/ci.yml)
[![Docker Repository on Quay](https://quay.io/repository/prometheuscommunity/prom-label-proxy/status "Docker Repository on Quay")](https://quay.io/repository/prometheuscommunity/prom-label-proxy)

The prom-label-proxy can enforce a given label in a given PromQL query, in Prometheus API responses or in Alertmanager API requests. As an example (but not only),
this allows read multi-tenancy for projects like Prometheus, Alertmanager or Thanos.

This proxy does not perform authentication or authorization, this has to happen before the request reaches this proxy, allowing you to use any authN/authZ system you want. The [kube-rbac-proxy](https://github.com/brancz/kube-rbac-proxy) is an example for such an additional building block. Additionally, you can use prom-label-proxy as a library in your own proxy, like what is done in [prom-authzed-proxy](https://github.com/authzed/prom-authzed-proxy).

### Risks outside the scope of this project

It's not a goal for this project to solve write tenant isolation for multi-tenant Prometheus:

* If a tenant controls its scrape target configuration the tenant can set arbitrary labels via its relabelling configuration, thereby being able to pollute other tenant's metrics.
* If the ingestion configuration [honor_labels](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) is set for a tenant's target, that target can pollute other tenant's metrics as Prometheus respects any labels exposed by the target.

See [Prometheus Operator](https://github.com/prometheus-operator/prometheus-operator) label enforcement, [Thanos soft/hard tenancy](https://thanos.io/tip/proposals/201812_thanos-remote-receive.md/#architecture) or [Cortex](https://cortexmetrics.io/) as example solution to that.

## Installing `prom-label-proxy`

### Helm

See: https://github.com/prometheus-community/helm-charts/tree/main/charts/prom-label-proxy

### Docker

We publish docker images for each release, see:

* [`quay.io/prometheuscommunity/prom-label-proxy`](https://quay.io/repository/prometheuscommunity/prom-label-proxy?tab=tags) for newest images
* `quay.io/coreos/prom-label-proxy:v0.1.0` for the initial v0.1.0 release.

### Building from source

If you want to build `prom-label-proxy` from source you would need a working installation of
the [Go](https://golang.org/) 1.15+ [toolchain](https://github.com/golang/tools) (`GOPATH`, `PATH=${GOPATH}/bin:${PATH}`).

`prom-label-proxy` can be downloaded and built by running:

```bash
go get github.com/prometheus-community/prom-label-proxy
```

## How does this project work?

This application proxies the following endpoints and it ensures that a particular label is enforced in the particular request and response:

* `/federate` for GET method (Prometheus)
* `/api/v1/query_exemplars` for GET and POST methods (Prometheus/Thanos)
* `/api/v1/query` for GET and POST methods (Prometheus/Thanos)
* `/api/v1/query_range` for GET and POST methods (Prometheus/Thanos)
* `/api/v1/series` for GET method (Prometheus/Thanos)
* `/api/v1/rules` for GET method (Prometheus/Thanos)
* `/api/v1/alerts` for GET method (Prometheus/Thanos)
* `/api/v2/silences` for GET and POST methods (Alertmanager)
* `/api/v2/silence/` for DELETE (Alertmanager)
* `/api/v2/alerts/groups` for GET (Alertmanager)
* `/api/v2/alerts` for GET (Alertmanager)

When started with the `-enable-label-apis` flag, the application can also proxy the following endpoints:

* `/api/v1/labels` for GET and POST methods (Prometheus/Thanos)
* `/api/v1/label/<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
Download .txt
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
Download .txt
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[![Build Status](https://github.com/prometheus-community/prom-label-proxy/actions/workflows/ci.yml/b"
  },
  {
    "path": "SECURITY.md",
    "chars": 220,
    "preview": "# Reporting a security issue\n\nThe Prometheus security policy, including how to report vulnerabilities, can be\nfound here"
  },
  {
    "path": "VERSION",
    "chars": 7,
    "preview": "0.13.0\n"
  },
  {
    "path": "dependabot.yml",
    "chars": 108,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": "examples/caddy-port-based/Caddyfile",
    "chars": 152,
    "preview": ":8081 {\n\nrewrite * ?{query}&job=pushgateway\nreverse_proxy 127.0.0.1:8080\n\n}\n\n:8082 {\n\nrewrite * ?{query}&job=prometheus\n"
  },
  {
    "path": "examples/caddy-port-based/README.md",
    "chars": 695,
    "preview": "# Demo: Using prom-label-proxy in front of demo Prometheus server.\n\n1. Run prom-label-proxy with passthrough option:\n\n``"
  },
  {
    "path": "examples/kube-rbac-proxy/Dockerfile",
    "chars": 62,
    "preview": "FROM alpine\n\nRUN apk add --no-cache curl\n\nCMD /bin/sleep 3600\n"
  },
  {
    "path": "examples/kube-rbac-proxy/client.yaml",
    "chars": 285,
    "preview": "---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: curl\nspec:\n  selector:\n    matchLabels:\n      name: curl\n  te"
  },
  {
    "path": "examples/kube-rbac-proxy/deployment.yaml",
    "chars": 2343,
    "preview": "---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: kube-rbac-proxy\n---\napiVersion: rbac.authorization.k8s.io/v1\nk"
  },
  {
    "path": "examples/kube-rbac-proxy/rbac.yaml",
    "chars": 473,
    "preview": "---\napiVersion: rbac.authorization.k8s.io/v1beta1\nkind: Role\nmetadata:\n  name: prom-label-proxy-client\nrules:\n  - apiGro"
  },
  {
    "path": "go.mod",
    "chars": 2743,
    "preview": "module github.com/prometheus-community/prom-label-proxy\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/efficientgo/core v1.0.0-rc.3\n\t"
  },
  {
    "path": "go.sum",
    "chars": 29856,
    "preview": "cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=\ncloud.google.com/go/auth v0.18.2/go.mod"
  },
  {
    "path": "injectproxy/alerts.go",
    "chars": 886,
    "preview": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\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.

Copied to clipboard!