[
  {
    "path": ".dockerignore",
    "content": "data/\n.build/\n.tarballs/\n\n!.build/linux-amd64/\n!.build/linux-arm64/\n!.build/linux-armv7/\n!.build/linux-ppc64le/\n!.build/linux-riscv64/\n!.build/linux-s390x/\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n    # Exclude configs synced from upstream prometheus/prometheus.\n    exclude-paths:\n      - .github/workflows/container_description.yml\n      - .github/workflows/golangci-lint.yml\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "---\nname: CI\non:\n  pull_request:\n  push:\n    branches: [main, master, 'release-*']\n    tags: ['v*']\n\npermissions:\n  contents: read\n\njobs:\n  test_go:\n    name: Go tests\n    runs-on: ubuntu-latest\n    container:\n      # Whenever the Go version is updated here, .promu.yml\n      # should also be updated.\n      image: quay.io/prometheus/golang-builder:1.26-base\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: prometheus/promci-setup@5af30ba8c199a91d6c04ebdc3c48e630e355f62d # v0.1.0\n      - run: make GO_ONLY=1 SKIP_GOLANGCI_LINT=1\n\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        thread: [ 0, 1, 2, 3]\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: prometheus/promci/build@9c86752f3395e08c57719af549cc455d8e2c2514 # v0.7.0\n        with:\n          parallelism: 4\n          thread: ${{ matrix.thread }}\n\n  publish_main:\n    name: Publish main branch artifacts\n    runs-on: ubuntu-latest\n    needs: [test_go, build]\n    if: |\n      (github.event_name == 'push' && github.event.ref == 'refs/heads/main')\n      ||\n      (github.event_name == 'push' && github.event.ref == 'refs/heads/master')\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: prometheus/promci/publish_main@9c86752f3395e08c57719af549cc455d8e2c2514 # v0.7.0\n        with:\n          docker_hub_organization: prometheuscommunity\n          docker_hub_password: ${{ secrets.docker_hub_password }}\n          quay_io_organization: prometheuscommunity\n          quay_io_password: ${{ secrets.quay_io_password }}\n\n  publish_release:\n    name: Publish release artefacts\n    runs-on: ubuntu-latest\n    needs: [test_go, build]\n    if: |\n      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: prometheus/promci/publish_release@9c86752f3395e08c57719af549cc455d8e2c2514 # v0.7.0\n        with:\n          docker_hub_organization: prometheuscommunity\n          docker_hub_password: ${{ secrets.docker_hub_password }}\n          quay_io_organization: prometheuscommunity\n          quay_io_password: ${{ secrets.quay_io_password }}\n          github_token: ${{ secrets.PROMBOT_GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/container_description.yml",
    "content": "---\nname: Push README to Docker Hub\non:\n  push:\n    paths:\n      - \"README.md\"\n      - \"README-containers.md\"\n      - \".github/workflows/container_description.yml\"\n    branches: [ main, master ]\n\npermissions:\n  contents: read\n\njobs:\n  PushDockerHubReadme:\n    runs-on: ubuntu-latest\n    name: Push README to Docker Hub\n    if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.\n    steps:\n      - name: git checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n      - name: Set docker hub repo name\n        run: echo \"DOCKER_REPO_NAME=$(make docker-repo-name)\" >> $GITHUB_ENV\n      - name: Push README to Dockerhub\n        uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1\n        env:\n          DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }}\n          DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }}\n        with:\n          destination_container_repo: ${{ env.DOCKER_REPO_NAME }}\n          provider: dockerhub\n          short_description: ${{ env.DOCKER_REPO_NAME }}\n          # Empty string results in README-containers.md being pushed if it\n          # exists. Otherwise, README.md is pushed.\n          readme_file: ''\n\n  PushQuayIoReadme:\n    runs-on: ubuntu-latest\n    name: Push README to quay.io\n    if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.\n    steps:\n      - name: git checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n      - name: Set quay.io org name\n        run: echo \"DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')\" >> $GITHUB_ENV\n      - name: Set quay.io repo name\n        run: echo \"DOCKER_REPO_NAME=$(make docker-repo-name)\" >> $GITHUB_ENV\n      - name: Push README to quay.io\n        uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1\n        env:\n          DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }}\n        with:\n          destination_container_repo: ${{ env.DOCKER_REPO_NAME }}\n          provider: quay\n          # Empty string results in README-containers.md being pushed if it\n          # exists. Otherwise, README.md is pushed.\n          readme_file: ''\n"
  },
  {
    "path": ".github/workflows/golangci-lint.yml",
    "content": "---\n# This action is synced from https://github.com/prometheus/prometheus\nname: golangci-lint\non:\n  push:\n    branches: [main, master, 'release-*']\n    paths:\n      - \"go.sum\"\n      - \"go.mod\"\n      - \"**.go\"\n      - \"scripts/errcheck_excludes.txt\"\n      - \".github/workflows/golangci-lint.yml\"\n      - \".golangci.yml\"\n    tags: ['v*']\n  pull_request:\n\npermissions:  # added using https://github.com/step-security/secure-repo\n  contents: read\n\njobs:\n  golangci:\n    permissions:\n      contents: read  # for actions/checkout to fetch code\n      pull-requests: read  # for golangci/golangci-lint-action to fetch pull requests\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n      - name: Install Go\n        uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0\n        with:\n          go-version: 1.26.x\n      - name: Install snmp_exporter/generator dependencies\n        run: sudo apt-get update && sudo apt-get -y install libsnmp-dev\n        if: github.repository == 'prometheus/snmp_exporter'\n      - name: Get golangci-lint version\n        id: golangci-lint-version\n        run: echo \"version=$(make print-golangci-lint-version)\" >> $GITHUB_OUTPUT\n      - name: Lint\n        uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0\n        with:\n          args: --verbose\n          version: ${{ steps.golangci-lint-version.outputs.version }}\n"
  },
  {
    "path": ".github/workflows/govulncheck.yml",
    "content": "---\nname: govulncheck\non:\n  pull_request:\n  push:\n    branches:\n      - main\n      - master\n  schedule:\n    - cron: '33 2 * * *'\n\npermissions:\n  contents: read\n\njobs:\n  govulncheck:\n    runs-on: ubuntu-latest\n    name: Run govulncheck\n    steps:\n      - id: govulncheck\n        uses: golang/govulncheck-action@31f7c5463448f83528bd771c2d978d940080c9fd # v1.0.4-unreleased\n"
  },
  {
    "path": ".gitignore",
    "content": "prom-label-proxy"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nlinters:\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    rules:\n      - linters:\n          - errcheck\n        path: _test.go\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\nformatters:\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".promu.yml",
    "content": "---\ngo:\n    # This must match .github/workflows/ci.yml.\n    version: 1.26\nrepository:\n    path: github.com/prometheus-community/prom-label-proxy\nbuild:\n    binaries:\n        - name: prom-label-proxy\n    # yamllint disable rule:line-length\n    ldflags: |\n        -X github.com/prometheus/common/version.Version={{.Version}}\n        -X github.com/prometheus/common/version.Revision={{.Revision}}\n        -X github.com/prometheus/common/version.Branch={{.Branch}}\n        -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}}\n        -X github.com/prometheus/common/version.BuildDate={{date \"20060102-15:04:05\"}}\n    # yamllint enable rule:line-length\ntarball:\n    files:\n        - LICENSE\ncrossbuild:\n    platforms:\n        - linux\n        - darwin\n        - windows\n        - freebsd\n        - openbsd\n        - netbsd\n"
  },
  {
    "path": ".yamllint",
    "content": "---\nextends: default\nignore: |\n  **/node_modules\n  web/api/v1/testdata/openapi_*_golden.yaml\n\nrules:\n  braces:\n    max-spaces-inside: 1\n    level: error\n  brackets:\n    max-spaces-inside: 1\n    level: error\n  commas: disable\n  comments: disable\n  comments-indentation: disable\n  document-start: disable\n  indentation:\n    spaces: consistent\n    indent-sequences: consistent\n  key-duplicates:\n    ignore: |\n      config/testdata/section_key_dup.bad.yml\n  line-length: disable\n  truthy:\n    check-keys: false\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## 0.13.0 / 2026-06-21\n\n* [FEATURE] Add the `-insecure-skip-verify` flag to bypass the TLS verification of the upstream server. #335\n* [FEATURE] Add the `-upstream-ca-cert` flag to provide the Certificate Authority's certificate of the upstream server. #340\n* [FEATURE] Add the `-enable-promql-extended-range-selectors` flag to support extended range selectors in PromQL expressions. #358\n* [FEATURE] Add the `-enable-promql-binop-fill-modifiers` flag to support binary operation fill modifiers in PromQL expressions. #358\n\n## 0.12.1 / 2025-09-11\n\n* [BUGFIX] Don't panic on `error-on-replace` with multiple values. #300\n\n## 0.12.0 / 2025-08-06\n\n* [ENHANCEMENT] Add the `-enable-promql-duration-expression-parsing` flag to support arithmetic for durations in PromQL expressions. #297\n* [ENHANCEMENT] Add the `-enable-promql-experimental-functions` flag to support experimental functions in PromQL expressions. #297\n* [ENHANCEMENT] Add the `-enable-label-matchers-for-rules-api` flag to filter rules using label matchers. #295\n\n## 0.11.1 / 2025-05-12\n\nRebuild with the latest Go compiler (`go1.24.3`).\n\n## 0.11.0 / 2024-08-07\n\n* [CHANGE] Return a 400 response code when the upstream response can't be modified. #228\n* [CHANGE] Make `-error-on-replace` more cooperating. #233\n* [FEATURE] Add the `-rules-with-active-alerts` flag to return rules with matching active alerts. #237\n\n## 0.10.0 / 2024-06-12\n\n* [FEATURE] Add the `header-uses-list-syntax` flag to split the tenant header value on commas. #223\n* [ENHANCEMENT] Support regex matcher for non-query Prometheus endpoints. #226\n\n## 0.9.0 / 2024-06-04\n\n* [ENHANCEMENT] Update /api/v1/{rules,alerts} responses. #214\n\n## 0.8.1 / 2024-01-28\n\nInternal change for library compatibility. No user-visible changes.\n\n* [CHANGE] Don't rely on slice labels #184\n\n## 0.8.0 / 2024-01-02\n\n* [FEATURE] Add the `--regex-match` flag to filter with a regexp matcher. #171\n\n## 0.7.0 / 2023-06-15\n\n* [FEATURE] Support filtering on multiple label values. #115\n\n## 0.6.0 / 2023-01-04\n\n* [FEATURE] Add the `--header-name` flag to pass the label value via HTTP header. #118\n* [FEATURE] Add the `--internal-listen-address` flag to expose Prometheus metrics. #121\n* [FEATURE] Add the the `--label-value` flag to set the label value statically. #116\n\n## 0.5.0 / 2022-06-14\n\n* [ENHANCEMENT] Add `/healthz` endpoint for (Kubernetes) probes. #106\n\n## 0.4.0 / 2021-10-05\n\n* [ENHANCEMENT] Support HTTP POST for /api/v1/labels endpoint. #70\n* [FEATURE] Add `--error-on-replace` flag (defaults to `false`) to return an error if a label value would otherwise be siltently replaced. #67\n* [ENHANCEMENT] Add label enforce support for the new query_exemplars API. #65\n\n## 0.3.0 / 2021-04-16\n\n* [FEATURE] Add support for /api/v1/series, /api/v1/labels and /api/v1/label/<name>/values endpoints (Prometheus/Thanos). #49\n* [FEATURE] Add `-passthrough-paths` flag (empty by default), which allows exposing chosen resources from upstream without enforcing (e.g Prometheus UI). #48\n* [ENHANCEMENT] Add support for queries via HTTP POST. #53\n\n## 0.2.0 / 2020-10-08\n\n* [FEATURE] Add support for /api/v1/rules (Prometheus/Thanos). #16\n* [FEATURE] Add support for /api/v1/alerts (Prometheus/Thanos). #18\n* [FEATURE] Add support for /api/v2/silences (Alertmanager). #20\n* [ENHANCEMENT] Enforce validity of the `-label` and `-upstream` CLI arguments. #33\n* [ENHANCEMENT] Allow multiple enforcement matchers. #39\n* [BUGFIX] Decompress gzipped response if needed. #35\n\n## 0.1.0 / 2018-10-24\n\nInitial release.\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Prometheus Community Code of Conduct\n\nPrometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).\n"
  },
  {
    "path": "Dockerfile",
    "content": "ARG ARCH=\"amd64\"\nARG OS=\"linux\"\nFROM quay.io/prometheus/busybox-${OS}-${ARCH}:glibc\nLABEL maintainer=\"The Prometheus Authors <prometheus-developers@googlegroups.com>\"\n\nARG ARCH=\"amd64\"\nARG OS=\"linux\"\nCOPY .build/${OS}-${ARCH}/prom-label-proxy /bin/prom-label-proxy\n\nUSER        nobody\nENTRYPOINT  [ \"/bin/prom-label-proxy\" ]\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "MAINTAINERS.md",
    "content": "# Maintainers\n\n* Lucas Servén Marín (lserven@gmail.com / @squat)\n* Simon Pasquier (pasquier.simon@gmail.com / @simonpasquier)\n"
  },
  {
    "path": "Makefile",
    "content": "# Copyright 2020 The Prometheus Authors\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# Needs to be defined before including Makefile.common to auto-generate targets\nDOCKER_ARCHS ?= amd64 arm64\nDOCKER_REPO  ?= prometheuscommunity\n\ninclude Makefile.common\n\nSTATICCHECK_IGNORE =\n\nDOCKER_IMAGE_NAME ?= prom-label-proxy\n\n.PHONY: run-curl-container\nrun-curl-container:\n\t@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'\n\tkubectl run -i -t krp-curl --image=quay.io/brancz/krp-curl:v0.0.1 --restart=Never --command -- /bin/sh\n"
  },
  {
    "path": "Makefile.common",
    "content": "# Copyright The Prometheus Authors\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\n# A common Makefile that includes rules to be reused in different prometheus projects.\n# !!! Open PRs only against the prometheus/prometheus/Makefile.common repository!\n\n# Example usage :\n# Create the main Makefile in the root project directory.\n# include Makefile.common\n# customTarget:\n# \t@echo \">> Running customTarget\"\n#\n\n# Ensure GOBIN is not set during build so that promu is installed to the correct path\nunexport GOBIN\n\nGO           ?= go\nGOFMT        ?= $(GO)fmt\nFIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH)))\nGOOPTS       ?=\nGOHOSTOS     ?= $(shell $(GO) env GOHOSTOS)\nGOHOSTARCH   ?= $(shell $(GO) env GOHOSTARCH)\n\nGO_VERSION        ?= $(shell $(GO) version)\nGO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION))\nPRE_GO_111        ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\\.(10|[0-9])\\.')\n\nPROMU        := $(FIRST_GOPATH)/bin/promu\npkgs          = ./...\n\nifeq (arm, $(GOHOSTARCH))\n\tGOHOSTARM ?= $(shell GOARM= $(GO) env GOARM)\n\tGO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM)\nelse\n\tGO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)\nendif\n\nGOTEST := $(GO) test\nGOTEST_DIR :=\nifneq ($(CIRCLE_JOB),)\nifneq ($(shell command -v gotestsum 2> /dev/null),)\n\tGOTEST_DIR := test-results\n\tGOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml --\nendif\nendif\n\nPROMU_VERSION ?= 0.18.1\nPROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz\n\nSKIP_GOLANGCI_LINT :=\nGOLANGCI_LINT :=\nGOLANGCI_LINT_OPTS ?=\nGOLANGCI_LINT_VERSION ?= v2.11.4\nGOLANGCI_FMT_OPTS ?=\n# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.\n# windows isn't included here because of the path separator being different.\nifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))\n\tifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64))\n\t\t# If we're in CI and there is an Actions file, that means the linter\n\t\t# is being run in Actions, so we don't need to run it here.\n\t\tifneq (,$(SKIP_GOLANGCI_LINT))\n\t\t\tGOLANGCI_LINT :=\n\t\telse ifeq (,$(CIRCLE_JOB))\n\t\t\tGOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint\n\t\telse ifeq (,$(wildcard .github/workflows/golangci-lint.yml))\n\t\t\tGOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint\n\t\tendif\n\tendif\nendif\n\nPREFIX                  ?= $(shell pwd)\nBIN_DIR                 ?= $(shell pwd)\nDOCKER_IMAGE_TAG        ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD))\nDOCKERBUILD_CONTEXT     ?= ./\nDOCKER_REPO             ?= prom\n\n# Check if deprecated DOCKERFILE_PATH is set\nifdef DOCKERFILE_PATH\n$(error DOCKERFILE_PATH is deprecated. Use DOCKERFILE_VARIANTS ?= $(DOCKERFILE_PATH) in the Makefile)\nendif\n\nDOCKER_ARCHS ?= amd64 arm64 armv7 ppc64le riscv64 s390x\nDOCKERFILE_VARIANTS     ?= $(wildcard Dockerfile Dockerfile.*)\n\n# Function to extract variant from Dockerfile label.\n# Returns the variant name from io.prometheus.image.variant label, or \"default\" if not found.\ndefine dockerfile_variant\n$(strip $(or $(shell sed -n 's/.*io\\.prometheus\\.image\\.variant=\"\\([^\"]*\\)\".*/\\1/p' $(1)),default))\nendef\n\n# Check for duplicate variant names (including default for Dockerfiles without labels).\nDOCKERFILE_VARIANT_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df)))\nDOCKERFILE_VARIANT_NAMES_SORTED := $(sort $(DOCKERFILE_VARIANT_NAMES))\nifneq ($(words $(DOCKERFILE_VARIANT_NAMES)),$(words $(DOCKERFILE_VARIANT_NAMES_SORTED)))\n$(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))\nendif\n\n# Build variant:dockerfile pairs for shell iteration.\nDOCKERFILE_VARIANTS_WITH_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df)):$(df))\n\nBUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS))\nPUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS))\nTAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS))\n\nSANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG))\n\nifeq ($(GOHOSTARCH),amd64)\n        ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows))\n                # Only supported on amd64\n                test-flags := -race\n        endif\nendif\n\n# This rule is used to forward a target like \"build\" to \"common-build\".  This\n# allows a new \"build\" target to be defined in a Makefile which includes this\n# one and override \"common-build\" without override warnings.\n%: common-% ;\n\n.PHONY: common-all\ncommon-all: precheck style check_license lint yamllint unused build test\n\n.PHONY: common-style\ncommon-style:\n\t@echo \">> checking code style\"\n\t@fmtRes=$$($(GOFMT) -d $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -name '*.go' -print)); \\\n\tif [ -n \"$${fmtRes}\" ]; then \\\n\t\techo \"gofmt checking failed!\"; echo \"$${fmtRes}\"; echo; \\\n\t\techo \"Please ensure you are using $$($(GO) version) for formatting code.\"; \\\n\t\texit 1; \\\n\tfi\n\n.PHONY: common-check_license\ncommon-check_license:\n\t@echo \">> checking license header\"\n\t@licRes=$$(for file in $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -type f -iname '*.go' -print) ; do \\\n               awk 'NR<=3' $$file | grep -Eq \"(Copyright|generated|GENERATED)\" || echo $$file; \\\n       done); \\\n       if [ -n \"$${licRes}\" ]; then \\\n               echo \"license header checking failed:\"; echo \"$${licRes}\"; \\\n               exit 1; \\\n       fi\n\t@echo \">> checking for copyright years 2026 or later\"\n\t@futureYearRes=$$(git grep -E 'Copyright (202[6-9]|20[3-9][0-9])' -- '*.go' ':!:vendor/*' || true); \\\n\tif [ -n \"$${futureYearRes}\" ]; then \\\n\t\techo \"Files with copyright year 2026 or later found (should use 'Copyright The Prometheus Authors'):\"; echo \"$${futureYearRes}\"; \\\n\t\texit 1; \\\n\tfi\n\n.PHONY: common-deps\ncommon-deps:\n\t@echo \">> getting dependencies\"\n\t$(GO) mod download\n\n.PHONY: update-go-deps\nupdate-go-deps:\n\t@echo \">> updating Go dependencies\"\n\t@for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \\\n\t\t$(GO) get $$m; \\\n\tdone\n\t$(GO) mod tidy\n\n.PHONY: common-test-short\ncommon-test-short: $(GOTEST_DIR)\n\t@echo \">> running short tests\"\n\t$(GOTEST) -short $(GOOPTS) $(pkgs)\n\n.PHONY: common-test\ncommon-test: $(GOTEST_DIR)\n\t@echo \">> running all tests\"\n\t$(GOTEST) $(test-flags) $(GOOPTS) $(pkgs)\n\n$(GOTEST_DIR):\n\t@mkdir -p $@\n\n.PHONY: common-format\ncommon-format: $(GOLANGCI_LINT)\n\t@echo \">> formatting code\"\n\t$(GO) fmt $(pkgs)\nifdef GOLANGCI_LINT\n\t@echo \">> formatting code with golangci-lint\"\n\t$(GOLANGCI_LINT) fmt $(GOLANGCI_FMT_OPTS)\nendif\n\n.PHONY: common-vet\ncommon-vet:\n\t@echo \">> vetting code\"\n\t$(GO) vet $(GOOPTS) $(pkgs)\n\n.PHONY: common-lint\ncommon-lint: $(GOLANGCI_LINT)\nifdef GOLANGCI_LINT\n\t@echo \">> running golangci-lint\"\n\t$(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs)\nendif\n\n.PHONY: common-lint-fix\ncommon-lint-fix: $(GOLANGCI_LINT)\nifdef GOLANGCI_LINT\n\t@echo \">> running golangci-lint fix\"\n\t$(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs)\nendif\n\n.PHONY: common-yamllint\ncommon-yamllint:\n\t@echo \">> running yamllint on all YAML files in the repository\"\nifeq (, $(shell command -v yamllint 2> /dev/null))\n\t@echo \"yamllint not installed so skipping\"\nelse\n\tyamllint .\nendif\n\n# For backward-compatibility.\n.PHONY: common-staticcheck\ncommon-staticcheck: lint\n\n.PHONY: common-unused\ncommon-unused:\n\t@echo \">> running check for unused/missing packages in go.mod\"\n\t$(GO) mod tidy\n\t@git diff --exit-code -- go.sum go.mod\n\n.PHONY: common-build\ncommon-build: promu\n\t@echo \">> building binaries\"\n\t$(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES)\n\n.PHONY: common-tarball\ncommon-tarball: promu\n\t@echo \">> building release tarball\"\n\t$(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR)\n\n.PHONY: common-docker-repo-name\ncommon-docker-repo-name:\n\t@echo \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)\"\n\n.PHONY: common-docker $(BUILD_DOCKER_ARCHS)\ncommon-docker: $(BUILD_DOCKER_ARCHS)\n$(BUILD_DOCKER_ARCHS): common-docker-%:\n\t@for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \\\n\t\tdockerfile=$${variant#*:}; \\\n\t\tvariant_name=$${variant%%:*}; \\\n\t\tdistroless_arch=\"$*\"; \\\n\t\tif [ \"$*\" = \"armv7\" ]; then \\\n\t\t\tdistroless_arch=\"arm\"; \\\n\t\tfi; \\\n\t\tif [ \"$$dockerfile\" = \"Dockerfile\" ]; then \\\n\t\t\techo \"Building default variant ($$variant_name) for linux-$* using $$dockerfile\"; \\\n\t\t\tdocker build -t \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)\" \\\n\t\t\t\t-f $$dockerfile \\\n\t\t\t\t--build-arg ARCH=\"$*\" \\\n\t\t\t\t--build-arg OS=\"linux\" \\\n\t\t\t\t--build-arg DISTROLESS_ARCH=\"$$distroless_arch\" \\\n\t\t\t\t$(DOCKERBUILD_CONTEXT); \\\n\t\t\tif [ \"$$variant_name\" != \"default\" ]; then \\\n\t\t\t\techo \"Tagging default variant with $$variant_name suffix\"; \\\n\t\t\t\tdocker tag \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)\" \\\n\t\t\t\t\t\"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\"; \\\n\t\t\tfi; \\\n\t\telse \\\n\t\t\techo \"Building $$variant_name variant for linux-$* using $$dockerfile\"; \\\n\t\t\tdocker build -t \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\" \\\n\t\t\t\t-f $$dockerfile \\\n\t\t\t\t--build-arg ARCH=\"$*\" \\\n\t\t\t\t--build-arg OS=\"linux\" \\\n\t\t\t\t--build-arg DISTROLESS_ARCH=\"$$distroless_arch\" \\\n\t\t\t\t$(DOCKERBUILD_CONTEXT); \\\n\t\tfi; \\\n\tdone\n\n.PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS)\ncommon-docker-publish: $(PUBLISH_DOCKER_ARCHS)\n$(PUBLISH_DOCKER_ARCHS): common-docker-publish-%:\n\t@for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \\\n\t\tdockerfile=$${variant#*:}; \\\n\t\tvariant_name=$${variant%%:*}; \\\n\t\tif [ \"$$dockerfile\" != \"Dockerfile\" ] || [ \"$$variant_name\" != \"default\" ]; then \\\n\t\t\techo \"Pushing $$variant_name variant for linux-$*\"; \\\n\t\t\tdocker push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\"; \\\n\t\tfi; \\\n\t\tif [ \"$$dockerfile\" = \"Dockerfile\" ]; then \\\n\t\t\techo \"Pushing default variant ($$variant_name) for linux-$*\"; \\\n\t\t\tdocker push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)\"; \\\n\t\tfi; \\\n\t\tif [ \"$(DOCKER_IMAGE_TAG)\" = \"latest\" ]; then \\\n\t\t\tif [ \"$$dockerfile\" != \"Dockerfile\" ] || [ \"$$variant_name\" != \"default\" ]; then \\\n\t\t\t\techo \"Pushing $$variant_name variant version tags for linux-$*\"; \\\n\t\t\t\tdocker push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name\"; \\\n\t\t\tfi; \\\n\t\t\tif [ \"$$dockerfile\" = \"Dockerfile\" ]; then \\\n\t\t\t\techo \"Pushing default variant version tag for linux-$*\"; \\\n\t\t\t\tdocker push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)\"; \\\n\t\t\tfi; \\\n\t\tfi; \\\n\tdone\n\nDOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION)))\n.PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS)\ncommon-docker-tag-latest: $(TAG_DOCKER_ARCHS)\n$(TAG_DOCKER_ARCHS): common-docker-tag-latest-%:\n\t@for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \\\n\t\tdockerfile=$${variant#*:}; \\\n\t\tvariant_name=$${variant%%:*}; \\\n\t\tif [ \"$$dockerfile\" != \"Dockerfile\" ] || [ \"$$variant_name\" != \"default\" ]; then \\\n\t\t\techo \"Tagging $$variant_name variant for linux-$* as latest\"; \\\n\t\t\tdocker tag \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\" \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest-$$variant_name\"; \\\n\t\t\tdocker 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\"; \\\n\t\tfi; \\\n\t\tif [ \"$$dockerfile\" = \"Dockerfile\" ]; then \\\n\t\t\techo \"Tagging default variant ($$variant_name) for linux-$* as latest\"; \\\n\t\t\tdocker tag \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)\" \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest\"; \\\n\t\t\tdocker tag \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)\" \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)\"; \\\n\t\tfi; \\\n\tdone\n\n.PHONY: common-docker-manifest\ncommon-docker-manifest:\n\t@for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \\\n\t\tdockerfile=$${variant#*:}; \\\n\t\tvariant_name=$${variant%%:*}; \\\n\t\tif [ \"$$dockerfile\" != \"Dockerfile\" ] || [ \"$$variant_name\" != \"default\" ]; then \\\n\t\t\techo \"Creating manifest for $$variant_name variant\"; \\\n\t\t\trefs=\"\"; \\\n\t\t\tfor arch in $(DOCKER_ARCHS); do \\\n\t\t\t\trefs=\"$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\"; \\\n\t\t\tdone; \\\n\t\t\tif [ -z \"$$refs\" ]; then \\\n\t\t\t\techo \"Skipping manifest for $$variant_name variant (no supported architectures)\"; \\\n\t\t\t\tcontinue; \\\n\t\t\tfi; \\\n\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\" $$refs; \\\n\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name\"; \\\n\t\tfi; \\\n\t\tif [ \"$$dockerfile\" = \"Dockerfile\" ]; then \\\n\t\t\techo \"Creating default variant ($$variant_name) manifest\"; \\\n\t\t\trefs=\"\"; \\\n\t\t\tfor arch in $(DOCKER_ARCHS); do \\\n\t\t\t\trefs=\"$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)\"; \\\n\t\t\tdone; \\\n\t\t\tif [ -z \"$$refs\" ]; then \\\n\t\t\t\techo \"Skipping default variant manifest (no supported architectures)\"; \\\n\t\t\t\tcontinue; \\\n\t\t\tfi; \\\n\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)\" $$refs; \\\n\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)\"; \\\n\t\tfi; \\\n\t\tif [ \"$(DOCKER_IMAGE_TAG)\" = \"latest\" ]; then \\\n\t\t\tif [ \"$$dockerfile\" != \"Dockerfile\" ] || [ \"$$variant_name\" != \"default\" ]; then \\\n\t\t\t\techo \"Creating manifest for $$variant_name variant version tag\"; \\\n\t\t\t\trefs=\"\"; \\\n\t\t\t\tfor arch in $(DOCKER_ARCHS); do \\\n\t\t\t\t\trefs=\"$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name\"; \\\n\t\t\t\tdone; \\\n\t\t\t\tif [ -z \"$$refs\" ]; then \\\n\t\t\t\t\techo \"Skipping version-tag manifest for $$variant_name variant (no supported architectures)\"; \\\n\t\t\t\t\tcontinue; \\\n\t\t\t\tfi; \\\n\t\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name\" $$refs; \\\n\t\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name\"; \\\n\t\t\tfi; \\\n\t\t\tif [ \"$$dockerfile\" = \"Dockerfile\" ]; then \\\n\t\t\t\techo \"Creating default variant version tag manifest\"; \\\n\t\t\t\trefs=\"\"; \\\n\t\t\t\tfor arch in $(DOCKER_ARCHS); do \\\n\t\t\t\t\trefs=\"$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)\"; \\\n\t\t\t\tdone; \\\n\t\t\t\tif [ -z \"$$refs\" ]; then \\\n\t\t\t\t\techo \"Skipping default variant version-tag manifest (no supported architectures)\"; \\\n\t\t\t\t\tcontinue; \\\n\t\t\t\tfi; \\\n\t\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)\" $$refs; \\\n\t\t\t\tDOCKER_CLI_EXPERIMENTAL=enabled docker manifest push \"$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)\"; \\\n\t\t\tfi; \\\n\t\tfi; \\\n\tdone\n\n.PHONY: promu\npromu: $(PROMU)\n\n$(PROMU):\n\t$(eval PROMU_TMP := $(shell mktemp -d))\n\tcurl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP)\n\tmkdir -p $(FIRST_GOPATH)/bin\n\tcp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu\n\trm -r $(PROMU_TMP)\n\n.PHONY: common-proto\ncommon-proto:\n\t@echo \">> generating code from proto files\"\n\t@./scripts/genproto.sh\n\nifdef GOLANGCI_LINT\n$(GOLANGCI_LINT):\n\tmkdir -p $(FIRST_GOPATH)/bin\n\tcurl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \\\n\t\t| sed -e '/install -d/d' \\\n\t\t| sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION)\nendif\n\n.PHONY: common-print-golangci-lint-version\ncommon-print-golangci-lint-version:\n\t@echo $(GOLANGCI_LINT_VERSION)\n\n.PHONY: precheck\nprecheck::\n\ndefine PRECHECK_COMMAND_template =\nprecheck:: $(1)_precheck\n\nPRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1)))\n.PHONY: $(1)_precheck\n$(1)_precheck:\n\t@if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \\\n\t\techo \"Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?\"; \\\n\t\texit 1; \\\n\tfi\nendef\n"
  },
  {
    "path": "OWNERS",
    "content": "component: \"Monitoring\"\n\nreviewers:\n- brancz\n- krasi-georgiev\n- metalmatze\n- paulfantom\n- pgier\n- s-urbaniak\n- simonpasquier\n- squat\n- lilic\n\napprovers:\n- brancz\n- krasi-georgiev\n- metalmatze\n- paulfantom\n- pgier\n- s-urbaniak\n- simonpasquier\n- squat\n- lilic\n"
  },
  {
    "path": "README.md",
    "content": "# prom-label-proxy\n\n[![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)\n[![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)\n\nThe 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),\nthis allows read multi-tenancy for projects like Prometheus, Alertmanager or Thanos.\n\nThis 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).\n\n### Risks outside the scope of this project\n\nIt's not a goal for this project to solve write tenant isolation for multi-tenant Prometheus:\n\n* 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.\n* 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.\n\nSee [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.\n\n## Installing `prom-label-proxy`\n\n### Helm\n\nSee: https://github.com/prometheus-community/helm-charts/tree/main/charts/prom-label-proxy\n\n### Docker\n\nWe publish docker images for each release, see:\n\n* [`quay.io/prometheuscommunity/prom-label-proxy`](https://quay.io/repository/prometheuscommunity/prom-label-proxy?tab=tags) for newest images\n* `quay.io/coreos/prom-label-proxy:v0.1.0` for the initial v0.1.0 release.\n\n### Building from source\n\nIf you want to build `prom-label-proxy` from source you would need a working installation of\nthe [Go](https://golang.org/) 1.15+ [toolchain](https://github.com/golang/tools) (`GOPATH`, `PATH=${GOPATH}/bin:${PATH}`).\n\n`prom-label-proxy` can be downloaded and built by running:\n\n```bash\ngo get github.com/prometheus-community/prom-label-proxy\n```\n\n## How does this project work?\n\nThis application proxies the following endpoints and it ensures that a particular label is enforced in the particular request and response:\n\n* `/federate` for GET method (Prometheus)\n* `/api/v1/query_exemplars` for GET and POST methods (Prometheus/Thanos)\n* `/api/v1/query` for GET and POST methods (Prometheus/Thanos)\n* `/api/v1/query_range` for GET and POST methods (Prometheus/Thanos)\n* `/api/v1/series` for GET method (Prometheus/Thanos)\n* `/api/v1/rules` for GET method (Prometheus/Thanos)\n* `/api/v1/alerts` for GET method (Prometheus/Thanos)\n* `/api/v2/silences` for GET and POST methods (Alertmanager)\n* `/api/v2/silence/` for DELETE (Alertmanager)\n* `/api/v2/alerts/groups` for GET (Alertmanager)\n* `/api/v2/alerts` for GET (Alertmanager)\n\nWhen started with the `-enable-label-apis` flag, the application can also proxy the following endpoints:\n\n* `/api/v1/labels` for GET and POST methods (Prometheus/Thanos)\n* `/api/v1/label/<name>/values` for GET method (Prometheus/Thanos)\n\nYou can run `prom-label-proxy` to enforce the value of the `tenant` label\nprovided in the client's request via the `tenant` HTTP query/form parameter:\n\n```\nprom-label-proxy \\\n   -query-param tenant \\\n   -label tenant \\\n   -upstream http://demo.do.prometheus.io:9090 \\\n   -insecure-listen-address 127.0.0.1:8080\n```\n\nAccessing the demo Prometheus APIs on `http://127.0.0.1:8080` will now expect\nthat the client's request provides the `tenant` label value using the `tenant`\nHTTP query parameter:\n\n```bash\n➜  ~ curl http://127.0.0.1:8080/api/v1/query\\?query=\"up\"\n{\"error\":\"The \\\"tenant\\\" query parameter must be provided.\",\"errorType\":\"prom-label-proxy\",\"status\":\"error\"}\n➜  ~ curl http://127.0.0.1:8080/api/v1/query\\?query=\"up\"\\&tenant\\=\"something\"\n{\"status\":\"success\",\"data\":{\"resultType\":\"vector\",\"result\":[]}}%\n```\n\nYou can provide multiple values for the label using several `tenant` HTTP query parameters:\n\n```bash\n➜  ~ curl http://127.0.0.1:8080/api/v1/query\\?query=\"up\"\\&tenant\\=\"something\"\\&tenant\\=\"anything\"\n{\"status\":\"success\",\"data\":{\"resultType\":\"vector\",\"result\":[]}}%\n```\n\nIt also works with POST requests:\n\n```bash\n➜  ~ curl http://127.0.0.1:8080/api/v1/query\" -d \"tenant=foo\"\n{\"status\":\"success\",\"data\":{\"resultType\":\"vector\",\"result\":[]}}%\n```\n\nAlternatively, `prom-label-proxy` can use a custom HTTP header instead HTTP parameters:\n\n```\nprom-label-proxy \\\n   -header-name X-Tenant \\\n   -label tenant \\\n   -upstream http://demo.do.prometheus.io:9090 \\\n   -insecure-listen-address 127.0.0.1:8080\n```\n\n```bash\n➜  ~ curl -H 'X-Tenant: something' http://127.0.0.1:8080/api/v1/query\\?query=\"up\"\n{\"status\":\"success\",\"data\":{\"resultType\":\"vector\",\"result\":[]}}%\n```\n\nYou can provide multiple values for the label using several HTTP headers:\n\n```bash\n➜  ~ curl -H 'X-Tenant=something' -H 'X-Tenant=anything' http://127.0.0.1:8080/api/v1/query\\?query=\"up\"\n{\"status\":\"success\",\"data\":{\"resultType\":\"vector\",\"result\":[]}}%\n```\n\nA last option is to provide a static value for the label:\n\n```\nprom-label-proxy \\\n   -label tenant \\\n   -label-value prometheus \\\n   -upstream http://demo.do.prometheus.io:9090 \\\n   -insecure-listen-address 127.0.0.1:8080\n```\n\nNow prom-label-proxy enforces the `tenant=\"prometheus\"` label in all requests.\n\nYou can provide multiple static values for a label. For example:\n\n```\nprom-label-proxy \\\n   -label tenant \\\n   -label-value prometheus \\\n   -label-value alertmanager \\\n   -upstream http://demo.do.prometheus.io:9090 \\\n   -insecure-listen-address 127.0.0.1:8080\n```\n\n`prom-label-proxy` will enforce the `tenant=~\"prometheus|alertmanager\"` label selector in all requests.\n\nYou can match the label value using a regular expression with the `-regex-match` option. For example:\n\n```\nprom-label-proxy \\\n   -label-value '^foo-.+$' \\\n   -label namespace \\\n   -upstream http://demo.do.prometheus.io:9090 \\\n   -insecure-listen-address 127.0.0.1:8080 \\\n   -regex-match\n```\n\n> :warning: The above feature is experimental. Be careful when using this option, it may expose sensitive metrics if you use a too permissive expression.\n\nTo 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:\n\n```\nprom-label-proxy \\\n   -header-name X-Namespace \\\n   -label namespace \\\n   -upstream http://demo.do.prometheus.io:9090 \\\n   -insecure-listen-address 127.0.0.1:8080 \\\n   -error-on-replace\n```\n\nOnce again for clarity: **this project only enforces a particular label in the respective calls to Prometheus, it in itself does not authenticate or\nauthorize the requesting entity in any way, this has to be built around this project.**\n\n### Federate endpoint\n\nThe 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).\n\n### Query endpoints\n\nFor 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.\n\nFor example, if requesting the PromQL query\n\n```\nhttp_requests_total{namespace=~\"a.*\"}\n```\n\nand specifying the namespace label must be enforced to `b`, then the query will be re-written to\n\n\n```\nhttp_requests_total{namespace=~\"b\"}\n```\n\nThis is enforced for any case, whether a label matcher is specified in the original query or not.\n\n### Metadata endpoints\n\nSimilar 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.\n\nNOTE: 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:\n* Prometheus >= [2.24.0](https://github.com/prometheus/prometheus/releases/tag/v2.24.0)\n* 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.\n\n### Rules endpoint\n\nThe 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.\n\nTo return alerting rules which have active alerts matching the label(s), you can use the `-rules-with-active-alerts` option. For example:\n\n```\nprom-label-proxy \\\n   -header-name X-Namespace \\\n   -label namespace \\\n   -upstream http://demo.do.prometheus.io:9090 \\\n   -insecure-listen-address 127.0.0.1:8080 \\\n   -rules-with-active-alerts\n```\n\nIf 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.\n\n### Alerts endpoint\n\nThe 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.\n\n### Silences endpoint\n\nThe proxy ensures the following:\n\n* `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.\n* `POST` requests to the `/api/v2/silences` endpoint can only affect silences that match the label and the label matcher is enforced.\n* `DELETE` requests to the `/api/v2/silence/` endpoint can only affect silences that match the label.\n\n:rotating_light: `prom-label-proxy` doesn't support multiple label values for the Silences endpoints :rotating_light:\n\n## Example use\n\nThe 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.).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Reporting a security issue\n\nThe Prometheus security policy, including how to report vulnerabilities, can be\nfound here:\n\n[https://prometheus.io/docs/operating/security/](https://prometheus.io/docs/operating/security/)\n"
  },
  {
    "path": "VERSION",
    "content": "0.13.0\n"
  },
  {
    "path": "dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": "examples/caddy-port-based/Caddyfile",
    "content": ":8081 {\n\nrewrite * ?{query}&job=pushgateway\nreverse_proxy 127.0.0.1:8080\n\n}\n\n:8082 {\n\nrewrite * ?{query}&job=prometheus\nreverse_proxy 127.0.0.1:8080\n\n}\n"
  },
  {
    "path": "examples/caddy-port-based/README.md",
    "content": "# Demo: Using prom-label-proxy in front of demo Prometheus server.\n\n1. Run prom-label-proxy with passthrough option:\n\n```\nprom-label-proxy -label job -upstream http://demo.robustperception.io:9090 -insecure-listen-address 127.0.0.1:8080 -non-api-path-passthrough\n```\n\n2. In separate terminal run caddy that injects job=prometheus when accessing localhost:8082 and injecting job=pushgateway on localhost:8081:\n\n```\ndocker run -it --rm --net=host -v $PWD/Caddyfile:/etc/caddy/Caddyfile caddy\n```\n\n3. Access `localhost:8082` and compare with original `http://demo.robustperception.io:9090` server. You should be able to access only `job=pushgateway`\nor `job=prometheus` data depending on the port.\n"
  },
  {
    "path": "examples/kube-rbac-proxy/Dockerfile",
    "content": "FROM alpine\n\nRUN apk add --no-cache curl\n\nCMD /bin/sleep 3600\n"
  },
  {
    "path": "examples/kube-rbac-proxy/client.yaml",
    "content": "---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: curl\nspec:\n  selector:\n    matchLabels:\n      name: curl\n  template:\n    metadata:\n      name: curl\n      labels:\n        name: curl\n    spec:\n      containers:\n        - name: curl\n          image: quay.io/brancz/curl:v0.0.1\n"
  },
  {
    "path": "examples/kube-rbac-proxy/deployment.yaml",
    "content": "---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: kube-rbac-proxy\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: kube-rbac-proxy\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: kube-rbac-proxy\nsubjects:\n  - kind: ServiceAccount\n    name: kube-rbac-proxy\n    namespace: default\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: kube-rbac-proxy\nrules:\n  - apiGroups: [\"authentication.k8s.io\"]\n    resources:\n      - tokenreviews\n    verbs: [\"create\"]\n  - apiGroups: [\"authorization.k8s.io\"]\n    resources:\n      - subjectaccessreviews\n    verbs: [\"create\"]\n---\napiVersion: v1\nkind: Service\nmetadata:\n  labels:\n    app: kube-rbac-proxy\n  name: kube-rbac-proxy\nspec:\n  ports:\n    - name: https\n      port: 8443\n      targetPort: https\n  selector:\n    app: kube-rbac-proxy\n---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: kube-rbac-proxy\ndata:\n  config.yaml: |+\n    authorization:\n      rewrites:\n        byQueryParameter:\n          name: \"namespace\"\n      resourceAttributes:\n        apiVersion: v1beta1\n        apiGroup: metrics.k8s.io\n        resource: pods\n        namespace: \"{{ .Value }}\"\n---\napiVersion: extensions/v1beta1\nkind: Deployment\nmetadata:\n  name: kube-rbac-proxy\nspec:\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: kube-rbac-proxy\n    spec:\n      serviceAccountName: kube-rbac-proxy\n      containers:\n        - name: kube-rbac-proxy\n          image: quay.io/brancz/kube-rbac-proxy:v0.4.0\n          args:\n            - \"--secure-listen-address=0.0.0.0:8443\"\n            - \"--upstream=http://localhost:8080/\"\n            - \"--config-file=/etc/kube-rbac-proxy/config.yaml\"\n            - \"--logtostderr=true\"\n            - \"--v=10\"\n          ports:\n            - containerPort: 8443\n              name: https\n          volumeMounts:\n            - name: config\n              mountPath: /etc/kube-rbac-proxy\n        - name: prom-label-enforcer\n          image: quay.io/coreos/prom-label-proxy:v0.1.0\n          imagePullPolicy: Always\n          args:\n            - \"--insecure-listen-address=127.0.0.1:8080\"\n            - \"--upstream=http://prometheus-k8s.monitoring.svc:9090/\"\n            - \"--label=namespace\"\n      volumes:\n        - name: config\n          configMap:\n            name: kube-rbac-proxy\n"
  },
  {
    "path": "examples/kube-rbac-proxy/rbac.yaml",
    "content": "---\napiVersion: rbac.authorization.k8s.io/v1beta1\nkind: Role\nmetadata:\n  name: prom-label-proxy-client\nrules:\n  - apiGroups: [\"metrics.k8s.io/v1beta1\"]\n    resources: [\"pods\"]\n    verbs: [\"get\"]\n---\napiVersion: rbac.authorization.k8s.io/v1beta1\nkind: RoleBinding\nmetadata:\n  name: prom-label-proxy-client\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: Role\n  name: prom-label-proxy-client\nsubjects:\n  - kind: ServiceAccount\n    name: default\n    namespace: default\n"
  },
  {
    "path": "go.mod",
    "content": "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\tgithub.com/go-openapi/runtime v0.29.3\n\tgithub.com/go-openapi/strfmt v0.26.1\n\tgithub.com/metalmatze/signal v0.0.0-20210307161603-1c9aa721a97a\n\tgithub.com/oklog/run v1.2.0\n\tgithub.com/prometheus/alertmanager v0.32.0\n\tgithub.com/prometheus/client_golang v1.23.2\n\tgithub.com/prometheus/prometheus v0.311.2\n\tgotest.tools/v3 v3.5.2\n)\n\nrequire (\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/dennwc/varint v1.0.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-openapi/analysis v0.25.0 // indirect\n\tgithub.com/go-openapi/errors v0.22.7 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.22.5 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.5 // indirect\n\tgithub.com/go-openapi/loads v0.23.3 // indirect\n\tgithub.com/go-openapi/spec v0.22.4 // indirect\n\tgithub.com/go-openapi/swag v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/cmdutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/conv v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/fileutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/jsonname v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/jsonutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/loading v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/mangling v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/netutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/stringutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/typeutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/yamlutils v0.25.5 // indirect\n\tgithub.com/go-openapi/validate v0.25.2 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/oklog/ulid/v2 v2.1.1 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.67.5 // indirect\n\tgithub.com/prometheus/procfs v0.16.1 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/otel v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.42.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.42.0 // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.4 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/net v0.52.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=\ncloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=\ngithub.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM=\ngithub.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=\ngithub.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=\ngithub.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=\ngithub.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.13 h1:5KgbxMaS2coSWRrx9TX/QtWbqzgQkOdEa3sZPhBhCSg=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.13/go.mod h1:8zz7wedqtCbw5e9Mi2doEwDyEgHcEE9YOJp6a8jdSMY=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.13 h1:mA59E3fokBvyEGHKFdnpNNrvaR351cqiHgRg+JzOSRI=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.13/go.mod h1:yoTXOQKea18nrM69wGF9jBdG4WocSZA1h38A+t/MAsk=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.14 h1:GcLE9ba5ehAQma6wlopUesYg/hbcOhFNWTjELkiWkh4=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.14/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 h1:mP49nTpfKtpXLt5SLn8Uv8z6W+03jYVoOSAl/c02nog=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw=\ngithub.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=\ngithub.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=\ngithub.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=\ngithub.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=\ngithub.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=\ngithub.com/efficientgo/core v1.0.0-rc.3 h1:X6CdgycYWDcbYiJr1H1+lQGzx13o7bq3EUkbB9DsSPc=\ngithub.com/efficientgo/core v1.0.0-rc.3/go.mod h1:FfGdkzWarkuzOlY04VY+bGfb1lWrjaL6x/GLcQ4vJps=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs=\ngithub.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE=\ngithub.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA=\ngithub.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w=\ngithub.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=\ngithub.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=\ngithub.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=\ngithub.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=\ngithub.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ=\ngithub.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA=\ngithub.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y=\ngithub.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI=\ngithub.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=\ngithub.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=\ngithub.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c=\ngithub.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y=\ngithub.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU=\ngithub.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA=\ngithub.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c=\ngithub.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=\ngithub.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=\ngithub.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=\ngithub.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk=\ngithub.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc=\ngithub.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=\ngithub.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=\ngithub.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=\ngithub.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=\ngithub.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=\ngithub.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=\ngithub.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw=\ngithub.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY=\ngithub.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU=\ngithub.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14=\ngithub.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=\ngithub.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=\ngithub.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=\ngithub.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=\ngithub.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=\ngithub.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q=\ngithub.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw=\ngithub.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=\ngithub.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0=\ngithub.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=\ngithub.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=\ngithub.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=\ngithub.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI=\ngithub.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE=\ngithub.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM=\ngithub.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=\ngithub.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/metalmatze/signal v0.0.0-20210307161603-1c9aa721a97a h1:0usWxe5SGXKQovz3p+BiQ81Jy845xSMu2CWKuXsXuUM=\ngithub.com/metalmatze/signal v0.0.0-20210307161603-1c9aa721a97a/go.mod h1:3OETvrxfELvGsU2RoGGWercfeZ4bCL3+SOwzIWtJH/Q=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=\ngithub.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=\ngithub.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=\ngithub.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=\ngithub.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/alertmanager v0.32.0 h1:JER/KWXvbmSo6cd8EtnP7y+0VWKG8RiYH+hV/hHNYio=\ngithub.com/prometheus/alertmanager v0.32.0/go.mod h1:0Dy9faTtMgpVYxJVxV0o65elTxHnSRCF/7gy5BKGZiE=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_golang/exp v0.0.0-20260325093428-d8591d0db856 h1:1Y6bmpZb8peQCy1IpctnAhIFuyhrdtMaDnETChhSNns=\ngithub.com/prometheus/client_golang/exp v0.0.0-20260325093428-d8591d0db856/go.mod h1:Vf0QcmVhGqpjLxZOaWrFSep86vchQtJmbztFaMM4f6Q=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=\ngithub.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=\ngithub.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=\ngithub.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=\ngithub.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=\ngithub.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=\ngithub.com/prometheus/prometheus v0.311.2 h1:6fBxp93y08GAZGNT1o3bIhgV/AMYvBFfU+ltDNEsHg8=\ngithub.com/prometheus/prometheus v0.311.2/go.mod h1:gjsCxTKtHO1Q8T9333u1s+lUR1OjPyM7ruuGH8RvVyo=\ngithub.com/prometheus/sigv4 v0.4.1 h1:EIc3j+8NBea9u1iV6O5ZAN8uvPq2xOIUPcqCTivHuXs=\ngithub.com/prometheus/sigv4 v0.4.1/go.mod h1:eu+ZbRvsc5TPiHwqh77OWuCnWK73IdkETYY46P4dXOU=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=\ngo.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=\ngo.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=\ngo.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=\ngo.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=\ngo.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=\ngo.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=\ngo.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=\ngo.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=\ngo.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=\ngolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=\ngolang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=\ngolang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=\ngoogle.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=\ngoogle.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\nk8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=\nk8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=\nk8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg=\nk8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c=\nk8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=\nk8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=\nk8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=\nk8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=\nk8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=\n"
  },
  {
    "path": "injectproxy/alerts.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage injectproxy\n\nimport \"net/http\"\n\n// alerts proxies HTTP requests to the Alertmanager /api/v2/alerts endpoint.\nfunc (r *routes) alerts(w http.ResponseWriter, req *http.Request) {\n\tswitch req.Method {\n\tcase \"GET\":\n\t\tr.enforceFilterParameter(w, req)\n\tdefault:\n\t\thttp.NotFound(w, req)\n\t}\n}\n"
  },
  {
    "path": "injectproxy/alerts_test.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage injectproxy\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestGetAlerts(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tlabelv         []string\n\t\tfilters        []string\n\t\texpCode        int\n\t\texpQueryValues []string\n\t\tqueryParam     string\n\t\turl            string\n\t}{\n\t\t{\n\t\t\t// No \"namespace\" parameter returns an error.\n\t\t\texpCode: http.StatusBadRequest,\n\t\t\turl:     \"http://alertmanager.example.com/api/v2/alerts\",\n\t\t},\n\t\t{\n\t\t\t// Check that other query parameters are not removed.\n\t\t\tlabelv:         []string{\"default\"},\n\t\t\texpCode:        http.StatusOK,\n\t\t\texpQueryValues: []string{\"false\"},\n\t\t\tqueryParam:     \"silenced\",\n\t\t\turl:            \"http://alertmanager.example.com/api/v2/alerts?silenced=false\",\n\t\t},\n\t\t{\n\t\t\t// Check that filter parameter is added when other query parameter are present.\n\t\t\tlabelv:         []string{\"default\"},\n\t\t\texpCode:        http.StatusOK,\n\t\t\texpQueryValues: []string{`namespace=\"default\"`},\n\t\t\tqueryParam:     \"filter\",\n\t\t\turl:            \"http://alertmanager.example.com/api/v2/alerts?silenced=false\",\n\t\t},\n\t\t{\n\t\t\t// Check that the filter parameter is added when multiple label values are set.\n\t\t\tlabelv:         []string{\"default\", \"something\"},\n\t\t\texpCode:        http.StatusOK,\n\t\t\texpQueryValues: []string{`namespace=~\"default|something\"`},\n\t\t\tqueryParam:     \"filter\",\n\t\t\turl:            \"http://alertmanager.example.com/api/v2/alerts?silenced=false\",\n\t\t},\n\t\t{\n\t\t\t// Check that the original filter parameter is preserved when multiple label values are set.\n\t\t\tlabelv:         []string{\"default\", \"something\"},\n\t\t\tfilters:        []string{`namespace=\"default\"`, `instance=~\".+\"`},\n\t\t\texpCode:        http.StatusOK,\n\t\t\texpQueryValues: []string{`namespace=~\"default|something\"`, `namespace=\"default\"`, `instance=~\".+\"`},\n\t\t\tqueryParam:     \"filter\",\n\t\t\turl:            \"http://alertmanager.example.com/api/v2/alerts?silenced=false\",\n\t\t},\n\t\t{\n\t\t\t// Check that label values are correctly escaped.\n\t\t\tlabelv:         []string{\"default\", \"some|thing\"},\n\t\t\texpCode:        http.StatusOK,\n\t\t\texpQueryValues: []string{`namespace=~\"default|some\\\\|thing\"`},\n\t\t\tqueryParam:     \"filter\",\n\t\t\turl:            \"http://alertmanager.example.com/api/v2/alerts?silenced=false\",\n\t\t},\n\t\t{\n\t\t\t// Check for filter parameter.\n\t\t\tlabelv:         []string{\"default\"},\n\t\t\tfilters:        []string{`job=\"prometheus\"`, `instance=~\".+\"`},\n\t\t\texpCode:        http.StatusOK,\n\t\t\texpQueryValues: []string{`job=\"prometheus\"`, `instance=~\".+\"`, `namespace=\"default\"`},\n\t\t\tqueryParam:     \"filter\",\n\t\t\turl:            \"http://alertmanager.example.com/api/v2/alerts\",\n\t\t},\n\t} {\n\t\tt.Run(strings.Join(tc.filters, \"&\"), func(t *testing.T) {\n\t\t\tm := newMockUpstream(checkQueryHandler(\"\", tc.queryParam, tc.expQueryValues...))\n\t\t\tdefer m.Close()\n\n\t\t\tr, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel})\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tu, err := url.Parse(tc.url)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t\tq := u.Query()\n\t\t\tfor _, m := range tc.filters {\n\t\t\t\tq.Add(\"filter\", m)\n\t\t\t}\n\n\t\t\tfor _, lv := range tc.labelv {\n\t\t\t\tq.Add(proxyLabel, lv)\n\t\t\t}\n\t\t\tu.RawQuery = q.Encode()\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", u.String(), nil)\n\t\t\tr.ServeHTTP(w, req)\n\n\t\t\tresp := w.Result()\n\t\t\tbody, _ := io.ReadAll(resp.Body)\n\t\t\tdefer resp.Body.Close()\n\n\t\t\tif resp.StatusCode != tc.expCode {\n\t\t\t\tt.Logf(\"expected status code %d, got %d\", tc.expCode, resp.StatusCode)\n\t\t\t\tt.Logf(\"%s\", string(body))\n\t\t\t\tt.FailNow()\n\t\t\t}\n\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "injectproxy/enforce.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage injectproxy\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\n\t\"github.com/prometheus/prometheus/model/labels\"\n\t\"github.com/prometheus/prometheus/promql/parser\"\n)\n\n// PromQLEnforcer can enforce label matchers in PromQL expressions.\ntype PromQLEnforcer struct {\n\tlabelMatchers  map[string]*labels.Matcher\n\terrorOnReplace bool\n\tparserOptions  parser.Options\n}\n\nfunc NewPromQLEnforcer(errorOnReplace bool, ms ...*labels.Matcher) *PromQLEnforcer {\n\treturn NewPromQLEnforcerWithOptions(errorOnReplace, defaultParserOptions(), ms...)\n}\n\nfunc NewPromQLEnforcerWithOptions(errorOnReplace bool, parserOptions parser.Options, ms ...*labels.Matcher) *PromQLEnforcer {\n\tentries := make(map[string]*labels.Matcher)\n\n\tfor _, matcher := range ms {\n\t\tentries[matcher.Name] = matcher\n\t}\n\n\treturn &PromQLEnforcer{\n\t\tlabelMatchers:  entries,\n\t\terrorOnReplace: errorOnReplace,\n\t\tparserOptions:  parserOptions,\n\t}\n}\n\nvar (\n\t// ErrQueryParse is returned when the input query is invalid.\n\tErrQueryParse = errors.New(\"failed to parse query string\")\n\n\t// ErrIllegalLabelMatcher is returned when the input query contains a conflicting label matcher.\n\tErrIllegalLabelMatcher = errors.New(\"conflicting label matcher\")\n\n\t// ErrEnforceLabel is returned when the label matchers couldn't be enforced.\n\tErrEnforceLabel = errors.New(\"failed to enforce label\")\n)\n\n// Enforce the label matchers in a PromQL expression.\nfunc (ms *PromQLEnforcer) Enforce(q string) (string, error) {\n\tp := parser.NewParser(ms.parserOptions)\n\texpr, err := p.ParseExpr(q)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"%w: %w\", ErrQueryParse, err)\n\t}\n\n\tif err := ms.EnforceNode(expr); err != nil {\n\t\tif errors.Is(err, ErrIllegalLabelMatcher) {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\treturn \"\", fmt.Errorf(\"%w: %w\", ErrEnforceLabel, err)\n\t}\n\n\treturn expr.String(), nil\n}\n\n// EnforceNode walks the given node recursively\n// and enforces the given label enforcer on it.\n//\n// Whenever a parser.MatrixSelector or parser.VectorSelector AST node is found,\n// their label enforcer is being potentially modified.\n// If a node's label matcher has the same name as a label matcher\n// of the given enforcer, then it will be replaced.\nfunc (ms PromQLEnforcer) EnforceNode(node parser.Node) error {\n\tswitch n := node.(type) {\n\tcase *parser.EvalStmt:\n\t\tif err := ms.EnforceNode(n.Expr); err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase parser.Expressions:\n\t\tfor _, e := range n {\n\t\t\tif err := ms.EnforceNode(e); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\tcase *parser.AggregateExpr:\n\t\tif err := ms.EnforceNode(n.Expr); err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase *parser.BinaryExpr:\n\t\tif err := ms.EnforceNode(n.LHS); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := ms.EnforceNode(n.RHS); err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase *parser.Call:\n\t\tif err := ms.EnforceNode(n.Args); err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase *parser.SubqueryExpr:\n\t\tif err := ms.EnforceNode(n.Expr); err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase *parser.ParenExpr:\n\t\tif err := ms.EnforceNode(n.Expr); err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase *parser.UnaryExpr:\n\t\tif err := ms.EnforceNode(n.Expr); err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase *parser.NumberLiteral, *parser.StringLiteral:\n\t// nothing to do\n\n\tcase *parser.MatrixSelector:\n\t\t// inject labelselector\n\t\tif vs, ok := n.VectorSelector.(*parser.VectorSelector); ok {\n\t\t\tvar err error\n\t\t\tvs.LabelMatchers, err = ms.EnforceMatchers(vs.LabelMatchers)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\tcase *parser.VectorSelector:\n\t\t// inject labelselector\n\t\tvar err error\n\t\tn.LabelMatchers, err = ms.EnforceMatchers(n.LabelMatchers)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\tdefault:\n\t\tpanic(fmt.Errorf(\"parser.Walk: unhandled node type %T\", n))\n\t}\n\n\treturn nil\n}\n\n// EnforceMatchers appends the enforced label matcher(s) to the list of matchers\n// if not already present.\n//\n// If the label matcher that is to be injected is present (by labelname), the\n// behavior depends on the errorOnReplace variable and the enforced matcher(s):\n// * If errorOnReplace is false\n//   - And the label matcher type is '=', the existing matcher is silently\n//     discarded whatever is the original value.\n//   - Otherwise the existing matcher is preserved.\n//\n// * if errorOnReplace is true\n//   - And the label matcher and the enforced matcher are disjoint, the function returns an error.\n//   - Otherwise the existing matcher is preserved.\nfunc (ms PromQLEnforcer) EnforceMatchers(targets []*labels.Matcher) ([]*labels.Matcher, error) {\n\tvar res []*labels.Matcher\n\n\tfor _, target := range targets {\n\t\tmatcher, ok := ms.labelMatchers[target.Name]\n\t\tif !ok {\n\t\t\tres = append(res, target)\n\t\t\tcontinue\n\t\t}\n\n\t\tif ms.errorOnReplace {\n\t\t\tvar ok bool\n\n\t\t\t// Ensure that the expression's matcher combined with the\n\t\t\t// enforced matchers can return some result. If the combined\n\t\t\t// matchers return no result, the function returns an error.\n\t\t\t//\n\t\t\t// For instance, when the enforced matcher is 'tenant=\"bar\"':\n\t\t\t// * and the expression's selector is 'tenant=\"foo\"' then the\n\t\t\t// result is always empty.\n\t\t\t// * and the expression's selector is 'tenant!=\"foo\"' then the\n\t\t\t// matchers don't conflict.\n\t\t\tswitch matcher.Type {\n\n\t\t\tcase labels.MatchEqual:\n\t\t\t\tswitch target.Type {\n\t\t\t\tcase labels.MatchEqual:\n\t\t\t\t\tok = matcher.Value == target.Value\n\t\t\t\tcase labels.MatchNotEqual:\n\t\t\t\t\tok = matcher.Value != target.Value\n\t\t\t\tcase labels.MatchRegexp:\n\t\t\t\t\tok = target.Matches(matcher.Value)\n\t\t\t\tcase labels.MatchNotRegexp:\n\t\t\t\t\tok = target.Matches(matcher.Value)\n\t\t\t\t}\n\n\t\t\tcase labels.MatchNotEqual:\n\t\t\t\tswitch target.Type {\n\t\t\t\tcase labels.MatchEqual:\n\t\t\t\t\tok = target.Value == \"\" || matcher.Matches(target.Value)\n\t\t\t\tcase labels.MatchNotEqual:\n\t\t\t\t\tok = true\n\t\t\t\tcase labels.MatchRegexp:\n\t\t\t\t\tfrm, _ := labels.NewFastRegexMatcher(target.Value)\n\t\t\t\t\tok = (frm == nil || len(frm.SetMatches()) == 0)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tfor _, sm := range frm.SetMatches() {\n\t\t\t\t\t\t\tif sm != matcher.Value {\n\t\t\t\t\t\t\t\tok = true\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tcase labels.MatchNotRegexp:\n\t\t\t\t\tok = true\n\t\t\t\t}\n\n\t\t\tcase labels.MatchRegexp:\n\t\t\t\tfrm, _ := labels.NewFastRegexMatcher(matcher.Value)\n\t\t\t\tswitch target.Type {\n\t\t\t\tcase labels.MatchEqual:\n\t\t\t\t\tok = matcher.Matches(target.Value)\n\t\t\t\tcase labels.MatchNotEqual:\n\t\t\t\t\tif frm != nil {\n\t\t\t\t\t\tif slices.ContainsFunc(frm.SetMatches(), target.Matches) {\n\t\t\t\t\t\t\tok = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tok = ok || (target.Value == \"\" && !matcher.Matches(\"\"))\n\t\t\t\tcase labels.MatchRegexp:\n\t\t\t\t\tif frm != nil {\n\t\t\t\t\t\tif slices.ContainsFunc(frm.SetMatches(), target.Matches) {\n\t\t\t\t\t\t\tok = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tcase labels.MatchNotRegexp:\n\t\t\t\t\tif frm != nil {\n\t\t\t\t\t\tif slices.ContainsFunc(frm.SetMatches(), target.Matches) {\n\t\t\t\t\t\t\tok = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tok = ok || (target.Value == \"\" && !matcher.Matches(\"\"))\n\t\t\t\t}\n\n\t\t\tcase labels.MatchNotRegexp:\n\t\t\t\tswitch target.Type {\n\t\t\t\tcase labels.MatchEqual:\n\t\t\t\t\tok = target.Value != \"\" || !matcher.Matches(\"\")\n\t\t\t\tcase labels.MatchNotEqual:\n\t\t\t\t\tok = true\n\t\t\t\tcase labels.MatchRegexp:\n\t\t\t\t\tfrm, _ := labels.NewFastRegexMatcher(target.Value)\n\t\t\t\t\tif frm != nil {\n\t\t\t\t\t\tif slices.ContainsFunc(frm.SetMatches(), matcher.Matches) {\n\t\t\t\t\t\t\tok = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tok = ok && (target.Value != \"\" || !matcher.Matches(\"\"))\n\t\t\t\t\tok = ok && target.Value != matcher.Value\n\t\t\t\tcase labels.MatchNotRegexp:\n\t\t\t\t\tok = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !ok {\n\t\t\t\treturn res, fmt.Errorf(\"%w: label matcher %q conflicts with injected matcher %q\", ErrIllegalLabelMatcher, target.String(), matcher.String())\n\t\t\t}\n\t\t}\n\n\t\t// Always drop the expression matcher if:\n\t\t// * the enforced matcher is an equal matcher because it will be\n\t\t// added after iterating on all the expression's matchers.\n\t\t// * or it is equal to the enforced matcher.\n\t\t// In both cases, the enforced matcher will be added after\n\t\t// iterating on all the expression's matchers.\n\t\tif matcher.Type == labels.MatchEqual || matcher.String() == target.String() {\n\t\t\tcontinue\n\t\t}\n\n\t\tres = append(res, target)\n\t}\n\n\tfor _, enforcedMatcher := range ms.labelMatchers {\n\t\tres = append(res, enforcedMatcher)\n\t}\n\n\treturn res, nil\n}\n\nfunc defaultParserOptions() parser.Options {\n\treturn parser.Options{}\n}\n"
  },
  {
    "path": "injectproxy/enforce_test.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage injectproxy\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/prometheus/prometheus/model/labels\"\n\t\"github.com/prometheus/prometheus/promql/parser\"\n)\n\nfunc mustNewMatcher(t labels.MatchType, n, v string) *labels.Matcher {\n\tm, err := labels.NewMatcher(t, n, v)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn m\n}\n\ntype checkFunc func(expression string, err error) error\n\nfunc checks(cs ...checkFunc) checkFunc {\n\treturn func(expression string, err error) error {\n\t\tfor _, c := range cs {\n\t\t\tif e := c(expression, err); e != nil {\n\t\t\t\treturn e\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc noError() checkFunc {\n\treturn func(_ string, got error) error {\n\t\tif got != nil {\n\t\t\treturn fmt.Errorf(\"want error <nil>, got %v\", got)\n\t\t}\n\n\t\treturn nil\n\t}\n}\n\nfunc errorIs(want error) checkFunc {\n\treturn func(_ string, got error) error {\n\t\tif errors.Is(got, want) {\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"want error of type %T, got %v\", want, got)\n\t}\n}\n\nfunc hasExpression(want string) checkFunc {\n\treturn func(got string, _ error) error {\n\t\tif want != got {\n\t\t\treturn fmt.Errorf(\"want expression \\n%v\\ngot \\n%v\", want, got)\n\t\t}\n\t\treturn nil\n\t}\n}\n\nvar tests = []struct {\n\tname       string\n\texpression string\n\tenforcer   *PromQLEnforcer\n\tcheck      checkFunc\n}{\n\t// first check correct label insertion\n\t{\n\t\tname:       \"expressions add label\",\n\t\texpression: `round(metric1{label=\"baz\"},3)`,\n\t\tenforcer: NewPromQLEnforcer(\n\t\t\tfalse,\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"namespace\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"NS\",\n\t\t\t},\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"pod\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"POD\",\n\t\t\t},\n\t\t),\n\t\tcheck: checks(\n\t\t\tnoError(),\n\t\t\thasExpression(`round(metric1{label=\"baz\",namespace=\"NS\",pod=\"POD\"}, 3)`),\n\t\t),\n\t},\n\n\t{\n\t\tname:       \"aggregate add label\",\n\t\texpression: `sum by (pod) (metric1{label=\"baz\"})`,\n\t\tenforcer: NewPromQLEnforcer(\n\t\t\tfalse,\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"namespace\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"NS\",\n\t\t\t},\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"pod\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"POD\",\n\t\t\t},\n\t\t),\n\t\tcheck: checks(\n\t\t\tnoError(),\n\t\t\thasExpression(`sum by (pod) (metric1{label=\"baz\",namespace=\"NS\",pod=\"POD\"})`),\n\t\t),\n\t},\n\n\t{\n\t\tname:       \"binary expression add label\",\n\t\texpression: `metric1{} + sum by (pod) (metric2{label=\"baz\"})`,\n\t\tenforcer: NewPromQLEnforcer(\n\t\t\tfalse,\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"namespace\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"NS\",\n\t\t\t},\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"pod\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"POD\",\n\t\t\t},\n\t\t),\n\t\tcheck: checks(\n\t\t\tnoError(),\n\t\t\thasExpression(`metric1{namespace=\"NS\",pod=\"POD\"} + sum by (pod) (metric2{label=\"baz\",namespace=\"NS\",pod=\"POD\"})`),\n\t\t),\n\t},\n\n\t{\n\t\tname:       \"binary expression with vector matching add label\",\n\t\texpression: `metric1{} + on(pod,namespace) sum by (pod) (metric2{label=\"baz\"})`,\n\t\tenforcer: NewPromQLEnforcer(\n\t\t\tfalse,\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"namespace\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"NS\",\n\t\t\t},\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"pod\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"POD\",\n\t\t\t},\n\t\t),\n\t\tcheck: checks(\n\t\t\tnoError(),\n\t\t\thasExpression(`metric1{namespace=\"NS\",pod=\"POD\"} + on (pod, namespace) sum by (pod) (metric2{label=\"baz\",namespace=\"NS\",pod=\"POD\"})`),\n\t\t),\n\t},\n\t// then check error return when a query would be silently altered, i.e. a label\n\t// matcher would be changed\n\t{\n\t\tname:       \"expressions error on non-matching label value\",\n\t\texpression: `round(metric1{label=\"baz\",pod=\"POD\",namespace=\"bar\"},3)`,\n\t\tenforcer: NewPromQLEnforcer(\n\t\t\ttrue,\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"namespace\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"NS\",\n\t\t\t},\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"pod\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"POD\",\n\t\t\t},\n\t\t),\n\t\tcheck: checks(\n\t\t\terrorIs(ErrIllegalLabelMatcher),\n\t\t),\n\t},\n\n\t{\n\t\tname:       \"aggregate error on non-matching label value\",\n\t\texpression: `sum by (pod) (metric1{label=\"baz\",pod=\"foo\",namespace=\"bar\"})`,\n\t\tenforcer: NewPromQLEnforcer(\n\t\t\ttrue,\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"namespace\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"NS\",\n\t\t\t},\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"pod\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"POD\",\n\t\t\t},\n\t\t),\n\t\tcheck: checks(\n\t\t\terrorIs(ErrIllegalLabelMatcher),\n\t\t),\n\t},\n\n\t{\n\t\tname:       \"binary expression error on non-matching label value\",\n\t\texpression: `metric1{pod=\"baz\"} + sum by (pod) (metric2{label=\"baz\",pod=\"foo\",namespace=\"bar\"})`,\n\t\tenforcer: NewPromQLEnforcer(\n\t\t\ttrue,\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"namespace\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"NS\",\n\t\t\t},\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"pod\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"POD\",\n\t\t\t},\n\t\t),\n\t\tcheck: checks(\n\t\t\terrorIs(ErrIllegalLabelMatcher),\n\t\t),\n\t},\n\n\t{\n\t\tname:       \"binary expression with vector matching error on non-matching label value\",\n\t\texpression: `metric1{pod=\"baz\"} + on (pod,namespace) sum by (pod) (metric2{label=\"baz\",pod=\"foo\",namespace=\"bar\"})`,\n\t\tenforcer: NewPromQLEnforcer(\n\t\t\ttrue,\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"namespace\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"NS\",\n\t\t\t},\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"pod\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"POD\",\n\t\t\t},\n\t\t),\n\t\tcheck: checks(\n\t\t\terrorIs(ErrIllegalLabelMatcher),\n\t\t),\n\t},\n\t// and lastly check that passing the label matcher we would inject\n\t// doesn't return an error\n\t{\n\t\tname:       \"expressions unchanged with matching label value\",\n\t\texpression: `round(metric1{label=\"baz\",pod=\"POD\",namespace=\"NS\"},3)`,\n\t\tenforcer: NewPromQLEnforcer(\n\t\t\tfalse,\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"namespace\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"NS\",\n\t\t\t},\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"pod\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"POD\",\n\t\t\t},\n\t\t),\n\t\tcheck: checks(\n\t\t\tnoError(),\n\t\t\thasExpression(`round(metric1{label=\"baz\",namespace=\"NS\",pod=\"POD\"}, 3)`),\n\t\t),\n\t},\n\n\t{\n\t\tname:       \"aggregate unchanged with matching label value\",\n\t\texpression: `sum by (pod) (metric1{label=\"baz\",pod=\"POD\",namespace=\"NS\"})`,\n\t\tenforcer: NewPromQLEnforcer(\n\t\t\tfalse,\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"namespace\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"NS\",\n\t\t\t},\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"pod\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"POD\",\n\t\t\t},\n\t\t),\n\t\tcheck: checks(\n\t\t\tnoError(),\n\t\t\thasExpression(`sum by (pod) (metric1{label=\"baz\",namespace=\"NS\",pod=\"POD\"})`),\n\t\t),\n\t},\n\n\t{\n\t\tname:       \"binary expression unchanged with matching label value\",\n\t\texpression: `metric1{pod=\"POD\"} + sum by (pod) (metric2{label=\"baz\",namespace=\"NS\",pod=\"POD\"})`,\n\t\tenforcer: NewPromQLEnforcer(\n\t\t\tfalse,\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"namespace\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"NS\",\n\t\t\t},\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"pod\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"POD\",\n\t\t\t},\n\t\t),\n\t\tcheck: checks(\n\t\t\tnoError(),\n\t\t\thasExpression(`metric1{namespace=\"NS\",pod=\"POD\"} + sum by (pod) (metric2{label=\"baz\",namespace=\"NS\",pod=\"POD\"})`),\n\t\t),\n\t},\n\n\t{\n\t\tname:       \"binary expression with vector matching unchanged with matching label value\",\n\t\texpression: `metric1{pod=\"POD\"} + on (pod,namespace) sum by (pod) (metric2{label=\"baz\",pod=\"POD\",namespace=\"NS\"})`,\n\t\tenforcer: NewPromQLEnforcer(\n\t\t\tfalse,\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"namespace\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"NS\",\n\t\t\t},\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"pod\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"POD\",\n\t\t\t},\n\t\t),\n\t\tcheck: checks(\n\t\t\tnoError(),\n\t\t\thasExpression(`metric1{namespace=\"NS\",pod=\"POD\"} + on (pod, namespace) sum by (pod) (metric2{label=\"baz\",namespace=\"NS\",pod=\"POD\"})`),\n\t\t),\n\t},\n\t{\n\t\tname:       \"invalid PromQL expression\",\n\t\texpression: `metric1{pod=\"baz\"`,\n\t\tenforcer: NewPromQLEnforcer(\n\t\t\tfalse,\n\t\t\t&labels.Matcher{\n\t\t\t\tName:  \"namespace\",\n\t\t\t\tType:  labels.MatchEqual,\n\t\t\t\tValue: \"NS\",\n\t\t\t},\n\t\t),\n\t\tcheck: checks(\n\t\t\terrorIs(ErrQueryParse),\n\t\t),\n\t},\n}\n\nfunc TestEnforce(t *testing.T) {\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot, err := tc.enforcer.Enforce(tc.expression)\n\t\t\tif err := tc.check(got, err); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEnforceWithErrOnReplace(t *testing.T) {\n\ttype subTestCase struct {\n\t\tlabelSelector string\n\t\texp           string\n\t\terr           bool\n\t}\n\n\tfor _, tc := range []struct {\n\t\tenforcedMatcher *labels.Matcher\n\t\tstcs            []subTestCase\n\t}{\n\t\t// Equal matcher enforcer.\n\t\t{\n\t\t\tenforcedMatcher: mustNewMatcher(labels.MatchEqual, \"job\", \"foo\"),\n\t\t\tstcs: []subTestCase{\n\t\t\t\t// No selector in the expression for the enforced label.\n\t\t\t\t{\n\t\t\t\t\t``,\n\t\t\t\t\t`up{job=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\n\t\t\t\t// Equal label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job=\"\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"foo\"`,\n\t\t\t\t\t`up{job=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"bar\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"fred\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\n\t\t\t\t// Not equal label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job!=\"\"`,\n\t\t\t\t\t`up{job=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!=\"foo\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!=\"bar\"`,\n\t\t\t\t\t`up{job=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!=\"fred\"`,\n\t\t\t\t\t`up{job=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\n\t\t\t\t// Regexp label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job=~\"\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"foo\"`,\n\t\t\t\t\t`up{job=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"bar\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"fred\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"foo|fred\"`,\n\t\t\t\t\t`up{job=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"foo|bar\"`,\n\t\t\t\t\t`up{job=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\n\t\t\t\t// Not-regexp label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job!~\"\"`,\n\t\t\t\t\t`up{job=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"bar\"`,\n\t\t\t\t\t`up{job=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"fred\"`,\n\t\t\t\t\t`up{job=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo|fred\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo|bar\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t// Not equal matcher enforcer.\n\t\t{\n\t\t\tenforcedMatcher: mustNewMatcher(labels.MatchNotEqual, \"job\", \"foo\"),\n\t\t\tstcs: []subTestCase{\n\t\t\t\t// No selector in the expression for the enforced label.\n\t\t\t\t{\n\t\t\t\t\t``,\n\t\t\t\t\t`up{job!=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\n\t\t\t\t// Equal label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job=\"\"`,\n\t\t\t\t\t`up{job!=\"foo\",job=\"\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"foo\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"bar\"`,\n\t\t\t\t\t`up{job!=\"foo\",job=\"bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"fred\"`,\n\t\t\t\t\t`up{job!=\"foo\",job=\"fred\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\n\t\t\t\t// Not equal label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job!=\"\"`,\n\t\t\t\t\t`up{job!=\"\",job!=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!=\"foo\"`,\n\t\t\t\t\t`up{job!=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!=\"bar\"`,\n\t\t\t\t\t`up{job!=\"bar\",job!=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!=\"fred\"`,\n\t\t\t\t\t`up{job!=\"foo\",job!=\"fred\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\n\t\t\t\t// Regexp label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job=~\"\"`,\n\t\t\t\t\t`up{job!=\"foo\",job=~\"\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t// up{job!=\"foo\",job=~\"foo\"} would return no result.\n\t\t\t\t\t`job=~\"foo\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"bar\"`,\n\t\t\t\t\t`up{job!=\"foo\",job=~\"bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"fred\"`,\n\t\t\t\t\t`up{job!=\"foo\",job=~\"fred\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"foo|fred\"`,\n\t\t\t\t\t`up{job!=\"foo\",job=~\"foo|fred\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"foo|bar\"`,\n\t\t\t\t\t`up{job!=\"foo\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\n\t\t\t\t// Not-regexp label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job!~\"\"`,\n\t\t\t\t\t`up{job!=\"foo\",job!~\"\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo\"`,\n\t\t\t\t\t`up{job!=\"foo\",job!~\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"bar\"`,\n\t\t\t\t\t`up{job!=\"foo\",job!~\"bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"fred\"`,\n\t\t\t\t\t`up{job!=\"foo\",job!~\"fred\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo|fred\"`,\n\t\t\t\t\t`up{job!=\"foo\",job!~\"foo|fred\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo|bar\"`,\n\t\t\t\t\t`up{job!=\"foo\",job!~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t// Regexp matcher enforcer.\n\t\t{\n\t\t\tenforcedMatcher: mustNewMatcher(labels.MatchRegexp, \"job\", \"foo|bar\"),\n\t\t\tstcs: []subTestCase{\n\t\t\t\t// No selector in the expression for the enforced label.\n\t\t\t\t{\n\t\t\t\t\t``,\n\t\t\t\t\t`up{job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\n\t\t\t\t// Equal label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job=\"\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"foo\"`,\n\t\t\t\t\t`up{job=\"foo\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"bar\"`,\n\t\t\t\t\t`up{job=\"bar\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"fred\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\n\t\t\t\t// Not equal label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job!=\"\"`,\n\t\t\t\t\t`up{job!=\"\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!=\"foo\"`,\n\t\t\t\t\t`up{job!=\"foo\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!=\"bar\"`,\n\t\t\t\t\t`up{job!=\"bar\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!=\"fred\"`,\n\t\t\t\t\t`up{job!=\"fred\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\n\t\t\t\t// Regexp label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job=~\"\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"foo\"`,\n\t\t\t\t\t`up{job=~\"foo\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"bar\"`,\n\t\t\t\t\t`up{job=~\"bar\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"fred\"`,\n\t\t\t\t\t`up{job=~\"foo|bar\",job=~\"fred\"}`,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"foo|fred\"`,\n\t\t\t\t\t`up{job=~\"foo|bar\",job=~\"foo|fred\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"foo|bar\"`,\n\t\t\t\t\t`up{job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\n\t\t\t\t// Not-regexp label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job!~\"\"`,\n\t\t\t\t\t`up{job!~\"\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo\"`,\n\t\t\t\t\t`up{job!~\"foo\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"bar\"`,\n\t\t\t\t\t`up{job!~\"bar\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"fred\"`,\n\t\t\t\t\t`up{job!~\"fred\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo|fred\"`,\n\t\t\t\t\t`up{job!~\"foo|fred\",job=~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo|bar\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t// Not regexp matcher enforcer.\n\t\t{\n\t\t\tenforcedMatcher: mustNewMatcher(labels.MatchNotRegexp, \"job\", \"foo|bar\"),\n\t\t\tstcs: []subTestCase{\n\t\t\t\t// No selector in the expression for the enforced label.\n\t\t\t\t{\n\t\t\t\t\t``,\n\t\t\t\t\t`up{job!~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\n\t\t\t\t// Equal label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job=\"\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"foo\"`,\n\t\t\t\t\t`up{job!~\"foo|bar\",job=\"foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"bar\"`,\n\t\t\t\t\t`up{job!~\"foo|bar\",job=\"bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"fred\"`,\n\t\t\t\t\t`up{job!~\"foo|bar\",job=\"fred\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\n\t\t\t\t// Not equal label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job!=\"\"`,\n\t\t\t\t\t`up{job!=\"\",job!~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!=\"foo\"`,\n\t\t\t\t\t`up{job!=\"foo\",job!~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!=\"bar\"`,\n\t\t\t\t\t`up{job!=\"bar\",job!~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!=\"fred\"`,\n\t\t\t\t\t`up{job!=\"fred\",job!~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\n\t\t\t\t// Regexp label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job=~\"\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t// up{job!~\"foo|bar\",job=~\"foo\"} would return no result.\n\t\t\t\t\t`job=~\"foo\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t// up{job!~\"foo|bar\",job=~\"bar\"} would return no result.\n\t\t\t\t\t`job=~\"bar\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"fred\"`,\n\t\t\t\t\t`up{job!~\"foo|bar\",job=~\"fred\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"foo|fred\"`,\n\t\t\t\t\t`up{job!~\"foo|bar\",job=~\"foo|fred\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"foo|bar\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\n\t\t\t\t// Not-regexp label selector in the expression.\n\t\t\t\t{\n\t\t\t\t\t`job!~\"\"`,\n\t\t\t\t\t`up{job!~\"\",job!~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo\"`,\n\t\t\t\t\t`up{job!~\"foo\",job!~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"bar\"`,\n\t\t\t\t\t`up{job!~\"bar\",job!~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"fred\"`,\n\t\t\t\t\t`up{job!~\"foo|bar\",job!~\"fred\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo|fred\"`,\n\t\t\t\t\t`up{job!~\"foo|bar\",job!~\"foo|fred\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo|bar\"`,\n\t\t\t\t\t`up{job!~\"foo|bar\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t// More edge cases.\n\t\t{\n\t\t\tenforcedMatcher: mustNewMatcher(labels.MatchRegexp, \"job\", \"foo|foo\"),\n\t\t\tstcs: []subTestCase{\n\t\t\t\t{\n\t\t\t\t\t`job!=\"foo\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"foo\"`,\n\t\t\t\t\t`up{job=\"foo\",job=~\"foo|foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"foo\",job=\"foo\"`,\n\t\t\t\t\t`up{job=\"foo\",job=\"foo\",job=~\"foo|foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=~\"foo\"`,\n\t\t\t\t\t`up{job=~\"foo\",job=~\"foo|foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"\"`,\n\t\t\t\t\t`up{job!~\"\",job=~\"foo|foo\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t// Regexp matcher enforcer which doesn't compile to a list of strings.\n\t\t{\n\t\t\tenforcedMatcher: mustNewMatcher(labels.MatchRegexp, \"job\", \"foo.*\"),\n\t\t\tstcs: []subTestCase{\n\t\t\t\t{\n\t\t\t\t\t// In theory, it should translate to\n\t\t\t\t\t// `up{job!=\"foo\",job=~\"foo.*\"}` but the enforcer can't\n\t\t\t\t\t// understand (yet) that both matchers are compatible.\n\t\t\t\t\t`job!=\"foo\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"foo\"`,\n\t\t\t\t\t`up{job=\"foo\",job=~\"foo.*\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job=\"foo\",job=\"foo\"`,\n\t\t\t\t\t`up{job=\"foo\",job=\"foo\",job=~\"foo.*\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t// In theory, it should translate to\n\t\t\t\t\t// `up{job=~\"foo\",job=~\"foo.*\"}` but the enforcer can't\n\t\t\t\t\t// understand (yet) that both matchers are compatible.\n\t\t\t\t\t`job=~\"foo\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t// In theory, it should translate to\n\t\t\t\t\t// `up{job!~\"foo\",job=~\"foo.*\"}` but the enforcer can't\n\t\t\t\t\t// understand (yet) that both matchers are compatible.\n\t\t\t\t\t`job!~\"foo\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"foo.*\"`,\n\t\t\t\t\t``,\n\t\t\t\t\ttrue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t`job!~\"\"`,\n\t\t\t\t\t`up{job!~\"\",job=~\"foo.*\"}`,\n\t\t\t\t\tfalse,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(fmt.Sprintf(\"enforcer=%q\", tc.enforcedMatcher.String()), func(t *testing.T) {\n\t\t\tenforcer := NewPromQLEnforcer(true, tc.enforcedMatcher)\n\n\t\t\tfor _, stc := range tc.stcs {\n\t\t\t\texpr := fmt.Sprintf(\"up{%s}\", stc.labelSelector)\n\t\t\t\tt.Run(expr, func(t *testing.T) {\n\t\t\t\t\tgot, err := enforcer.Enforce(expr)\n\t\t\t\t\tif stc.err {\n\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\tt.Fatalf(\"expected error, got nil\")\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !errors.Is(err, ErrIllegalLabelMatcher) {\n\t\t\t\t\t\t\tt.Fatalf(\"expected err,ErrIllegalLabelMatcher error, got %s\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"expected no error, got %s\", err.Error())\n\t\t\t\t\t}\n\n\t\t\t\t\tif got != stc.exp {\n\t\t\t\t\t\tt.Fatalf(\"expected expression %q, got %q\", stc.exp, got)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEnforceWithParserOptions(t *testing.T) {\n\tns := mustNewMatcher(labels.MatchEqual, \"namespace\", \"NS\")\n\n\tfor _, tc := range []struct {\n\t\tname       string\n\t\texpression string\n\t\tenforcer   *PromQLEnforcer\n\t\tcheck      checkFunc\n\t}{\n\t\t// EnableExperimentalFunctions\n\t\t{\n\t\t\tname:       \"experimental function enabled\",\n\t\t\texpression: `mad_over_time(metric[5m])`,\n\t\t\tenforcer:   NewPromQLEnforcerWithOptions(false, parser.Options{EnableExperimentalFunctions: true}, ns),\n\t\t\tcheck: checks(\n\t\t\t\tnoError(),\n\t\t\t\thasExpression(`mad_over_time(metric{namespace=\"NS\"}[5m])`),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname:       \"experimental function disabled\",\n\t\t\texpression: `mad_over_time(metric[5m])`,\n\t\t\tenforcer:   NewPromQLEnforcerWithOptions(false, parser.Options{}, ns),\n\t\t\tcheck:      errorIs(ErrQueryParse),\n\t\t},\n\n\t\t// ExperimentalDurationExpr\n\t\t{\n\t\t\tname:       \"experimental duration expression enabled\",\n\t\t\texpression: `rate(metric[5m * 2])`,\n\t\t\tenforcer:   NewPromQLEnforcerWithOptions(false, parser.Options{ExperimentalDurationExpr: true}, ns),\n\t\t\tcheck: checks(\n\t\t\t\tnoError(),\n\t\t\t\thasExpression(`rate(metric{namespace=\"NS\"}[5m * 2])`),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname:       \"experimental duration expression disabled\",\n\t\t\texpression: `rate(metric[5m * 2])`,\n\t\t\tenforcer:   NewPromQLEnforcerWithOptions(false, parser.Options{}, ns),\n\t\t\tcheck:      errorIs(ErrQueryParse),\n\t\t},\n\n\t\t// EnableExtendedRangeSelectors\n\t\t{\n\t\t\tname:       \"extended range selectors enabled\",\n\t\t\texpression: `metric anchored`,\n\t\t\tenforcer:   NewPromQLEnforcerWithOptions(false, parser.Options{EnableExtendedRangeSelectors: true}, ns),\n\t\t\tcheck: checks(\n\t\t\t\tnoError(),\n\t\t\t\thasExpression(`metric{namespace=\"NS\"} anchored`),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname:       \"extended range selectors disabled\",\n\t\t\texpression: `metric anchored`,\n\t\t\tenforcer:   NewPromQLEnforcerWithOptions(false, parser.Options{}, ns),\n\t\t\tcheck:      errorIs(ErrQueryParse),\n\t\t},\n\n\t\t// EnableBinopFillModifiers\n\t\t{\n\t\t\tname:       \"binop fill modifiers enabled\",\n\t\t\texpression: `metric_a + fill(0) metric_b`,\n\t\t\tenforcer:   NewPromQLEnforcerWithOptions(false, parser.Options{EnableBinopFillModifiers: true}, ns),\n\t\t\tcheck: checks(\n\t\t\t\tnoError(),\n\t\t\t\thasExpression(`metric_a{namespace=\"NS\"} + fill (0) metric_b{namespace=\"NS\"}`),\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname:       \"binop fill modifiers disabled\",\n\t\t\texpression: `metric_a + fill(0) metric_b`,\n\t\t\tenforcer:   NewPromQLEnforcerWithOptions(false, parser.Options{}, ns),\n\t\t\tcheck:      errorIs(ErrQueryParse),\n\t\t},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot, err := tc.enforcer.Enforce(tc.expression)\n\t\t\tif err := tc.check(got, err); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "injectproxy/routes.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage injectproxy\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/efficientgo/core/merrors\"\n\t\"github.com/metalmatze/signal/server/signalhttp\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/prometheus/model/labels\"\n\t\"github.com/prometheus/prometheus/promql/parser\"\n)\n\nconst (\n\tqueryParam    = \"query\"\n\tmatchersParam = \"match[]\"\n)\n\ntype routes struct {\n\tupstream *url.URL\n\thandler  http.Handler\n\tlabel    string\n\tel       ExtractLabeler\n\n\tmux                   http.Handler\n\tmodifiers             map[string]func(*http.Response) error\n\terrorOnReplace        bool\n\tregexMatch            bool\n\trulesWithActiveAlerts bool\n\tparserOpts            parser.Options\n\n\tlogger *log.Logger\n}\n\ntype options struct {\n\tupstreamCaCert           string\n\tenableLabelAPIs          bool\n\tpassthroughPaths         []string\n\tinsecureSkipVerify       bool\n\terrorOnReplace           bool\n\tregisterer               prometheus.Registerer\n\tregexMatch               bool\n\trulesWithActiveAlerts    bool\n\tlabelMatchersForRulesAPI bool\n\tparserOptions            parser.Options\n}\n\ntype Option interface {\n\tapply(*options)\n}\n\ntype optionFunc func(*options)\n\nfunc (f optionFunc) apply(o *options) {\n\tf(o)\n}\n\n// WithPrometheusRegistry configures the proxy to use the given registerer.\nfunc WithPrometheusRegistry(reg prometheus.Registerer) Option {\n\treturn optionFunc(func(o *options) {\n\t\to.registerer = reg\n\t})\n}\n\n// WithUpstreamCaCert configures the proxy to use the custom ca certificate for the upstream.\nfunc WithUpstreamCaCert(caCert string) Option {\n\treturn optionFunc(func(o *options) {\n\t\to.upstreamCaCert = caCert\n\t})\n}\n\n// WithEnabledLabelsAPI enables proxying to labels API. If false, \"501 Not implemented\" will be return for those.\nfunc WithEnabledLabelsAPI() Option {\n\treturn optionFunc(func(o *options) {\n\t\to.enableLabelAPIs = true\n\t})\n}\n\n// WithPassthroughPaths configures routes to register given paths as passthrough handlers for all HTTP methods.\n// that, if requested, will be forwarded without enforcing label. Use with care.\n// NOTE: Passthrough \"all\" paths like \"/\" or \"\" and regex are not allowed.\nfunc WithPassthroughPaths(paths []string) Option {\n\treturn optionFunc(func(o *options) {\n\t\to.passthroughPaths = paths\n\t})\n}\n\n// insecureSkipVerify configures proxy to bypass validation of the server's TLS/SSL certificate.\nfunc WithInsecureSkipVerify() Option {\n\treturn optionFunc(func(o *options) {\n\t\to.insecureSkipVerify = true\n\t})\n}\n\n// WithErrorOnReplace causes the proxy to return 400 if a label matcher we want to\n// inject is present in the query already and matches something different\nfunc WithErrorOnReplace() Option {\n\treturn optionFunc(func(o *options) {\n\t\to.errorOnReplace = true\n\t})\n}\n\n// WithActiveAlerts causes the proxy to return rules with active alerts.\nfunc WithActiveAlerts() Option {\n\treturn optionFunc(func(o *options) {\n\t\to.rulesWithActiveAlerts = true\n\t})\n}\n\n// WithLabelMatchersForRulesAPI instructs the proxy to use label matchers when querying the Rules API.\nfunc WithLabelMatchersForRulesAPI() Option {\n\treturn optionFunc(func(o *options) {\n\t\to.labelMatchersForRulesAPI = true\n\t})\n}\n\n// WithRegexMatch causes the proxy to handle tenant name as regexp\nfunc WithRegexMatch() Option {\n\treturn optionFunc(func(o *options) {\n\t\to.regexMatch = true\n\t})\n}\n\n// WithPromqlDurationExpressionParsing enables parsing of duration expressions in the PromQL parser.\nfunc WithPromqlDurationExpressionParsing() Option {\n\treturn optionFunc(func(o *options) {\n\t\to.parserOptions.ExperimentalDurationExpr = true\n\t})\n}\n\n// WithPromqlExperimentalFunctions enables parsing of experimental functions in the PromQL parser.\nfunc WithPromqlExperimentalFunctions() Option {\n\treturn optionFunc(func(o *options) {\n\t\to.parserOptions.EnableExperimentalFunctions = true\n\t})\n}\n\n// WithPromqlExtendedRangeSelectors enables extended range selectors in the PromQL parser.\nfunc WithPromqlExtendedRangeSelectors() Option {\n\treturn optionFunc(func(o *options) {\n\t\to.parserOptions.EnableExtendedRangeSelectors = true\n\t})\n}\n\n// WithPromqlBinopFillModifiers enables binary operation fill modifiers in the PromQL parser.\nfunc WithPromqlBinopFillModifiers() Option {\n\treturn optionFunc(func(o *options) {\n\t\to.parserOptions.EnableBinopFillModifiers = true\n\t})\n}\n\n// mux abstracts away the behavior we expect from the http.ServeMux type in this package.\ntype mux interface {\n\thttp.Handler\n\tHandle(string, http.Handler)\n}\n\n// strictMux is a mux that wraps standard HTTP handler with safer handler that allows safe user provided handler registrations.\ntype strictMux struct {\n\tmux\n\tseen map[string]struct{}\n}\n\nfunc newStrictMux(m mux) *strictMux {\n\treturn &strictMux{\n\t\tm,\n\t\tmap[string]struct{}{},\n\t}\n\n}\n\n// Handle is like HTTP mux handle but it does not allow to register paths that are shared with previously registered paths.\n// It also makes sure the trailing / is registered too.\n// For example if /api/v1/federate was registered consequent registrations like /api/v1/federate/ or /api/v1/federate/some will\n// 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\n// registration.\n// This allows to de-risk ability for user to mis-configure and leak inject isolation.\nfunc (s *strictMux) Handle(pattern string, handler http.Handler) error {\n\tsanitized := pattern\n\tfor next := strings.TrimSuffix(sanitized, \"/\"); next != sanitized; sanitized = next {\n\t}\n\n\tif _, ok := s.seen[sanitized]; ok {\n\t\treturn fmt.Errorf(\"pattern %q was already registered\", sanitized)\n\t}\n\n\tfor p := range s.seen {\n\t\tif strings.HasPrefix(sanitized+\"/\", p+\"/\") {\n\t\t\treturn fmt.Errorf(\"pattern %q is registered, cannot register path %q that shares it\", p, sanitized)\n\t\t}\n\t}\n\n\ts.mux.Handle(sanitized, handler)\n\ts.mux.Handle(sanitized+\"/\", handler)\n\ts.seen[sanitized] = struct{}{}\n\n\treturn nil\n}\n\n// instrumentedMux wraps a mux and instruments it.\ntype instrumentedMux struct {\n\tmux\n\ti signalhttp.HandlerInstrumenter\n}\n\nfunc newInstrumentedMux(m mux, r prometheus.Registerer) *instrumentedMux {\n\treturn &instrumentedMux{\n\t\tm,\n\t\tsignalhttp.NewHandlerInstrumenter(r, []string{\"handler\"}),\n\t}\n}\n\n// Handle implements the mux interface.\nfunc (i *instrumentedMux) Handle(pattern string, handler http.Handler) {\n\ti.mux.Handle(pattern, i.i.NewHandler(prometheus.Labels{\"handler\": pattern}, handler))\n}\n\n// ExtractLabeler is an HTTP handler that extract the label value to be\n// enforced from the HTTP request.  If a valid label value is found, it should\n// store it in the request's context.  Otherwise it should return an error in\n// the HTTP response (usually 400 or 500).\ntype ExtractLabeler interface {\n\tExtractLabel(next http.HandlerFunc) http.Handler\n}\n\n// HTTPFormEnforcer enforces a label value extracted from the HTTP form and query parameters.\ntype HTTPFormEnforcer struct {\n\tParameterName string\n}\n\n// ExtractLabel implements the ExtractLabeler interface.\nfunc (hff HTTPFormEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tlabelValues, err := hff.getLabelValues(r)\n\t\tif err != nil {\n\t\t\tprometheusAPIError(w, humanFriendlyErrorMessage(err), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\t// Remove the proxy label from the query parameters.\n\t\tq := r.URL.Query()\n\t\tq.Del(hff.ParameterName)\n\t\tr.URL.RawQuery = q.Encode()\n\n\t\t// Remove the param from the PostForm.\n\t\tif r.Method == http.MethodPost {\n\t\t\tif err := r.ParseForm(); err != nil {\n\t\t\t\tprometheusAPIError(w, fmt.Sprintf(\"Failed to parse the PostForm: %v\", err), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif r.PostForm.Get(hff.ParameterName) != \"\" {\n\t\t\t\tr.PostForm.Del(hff.ParameterName)\n\t\t\t\tnewBody := r.PostForm.Encode()\n\t\t\t\t// We are replacing request body, close previous one (r.FormValue ensures it is read fully and not nil).\n\t\t\t\t_ = r.Body.Close()\n\t\t\t\tr.Body = io.NopCloser(strings.NewReader(newBody))\n\t\t\t\tr.ContentLength = int64(len(newBody))\n\t\t\t}\n\t\t}\n\n\t\tnext.ServeHTTP(w, r.WithContext(WithLabelValues(r.Context(), labelValues)))\n\t})\n}\n\nfunc (hff HTTPFormEnforcer) getLabelValues(r *http.Request) ([]string, error) {\n\terr := r.ParseForm()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"the form data can not be parsed: %w\", err)\n\t}\n\n\tformValues := removeEmptyValues(r.Form[hff.ParameterName])\n\tif len(formValues) == 0 {\n\t\treturn nil, fmt.Errorf(\"the %q query parameter must be provided\", hff.ParameterName)\n\t}\n\n\treturn formValues, nil\n}\n\n// HTTPHeaderEnforcer enforces a label value extracted from the HTTP headers.\ntype HTTPHeaderEnforcer struct {\n\tName            string\n\tParseListSyntax bool\n}\n\n// ExtractLabel implements the ExtractLabeler interface.\nfunc (hhe HTTPHeaderEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tlabelValues, err := hhe.getLabelValues(r)\n\t\tif err != nil {\n\t\t\tprometheusAPIError(w, humanFriendlyErrorMessage(err), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tnext.ServeHTTP(w, r.WithContext(WithLabelValues(r.Context(), labelValues)))\n\t})\n}\n\nfunc (hhe HTTPHeaderEnforcer) getLabelValues(r *http.Request) ([]string, error) {\n\theaderValues := r.Header[hhe.Name]\n\n\tif hhe.ParseListSyntax {\n\t\theaderValues = trimValues(splitValues(headerValues, \",\"))\n\t}\n\n\theaderValues = removeEmptyValues(headerValues)\n\n\tif len(headerValues) == 0 {\n\t\treturn nil, fmt.Errorf(\"missing HTTP header %q\", hhe.Name)\n\t}\n\n\treturn headerValues, nil\n}\n\n// StaticLabelEnforcer enforces a static label value.\ntype StaticLabelEnforcer []string\n\n// ExtractLabel implements the ExtractLabeler interface.\nfunc (sle StaticLabelEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tnext(w, r.WithContext(WithLabelValues(r.Context(), sle)))\n\t})\n}\n\nfunc NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, opts ...Option) (*routes, error) {\n\topt := options{}\n\tfor _, o := range opts {\n\t\to.apply(&opt)\n\t}\n\n\tif opt.registerer == nil {\n\t\topt.registerer = prometheus.NewRegistry()\n\t}\n\n\tproxy := httputil.NewSingleHostReverseProxy(upstream)\n\n\tr := &routes{\n\t\tupstream:              upstream,\n\t\thandler:               proxy,\n\t\tlabel:                 label,\n\t\tel:                    extractLabeler,\n\t\terrorOnReplace:        opt.errorOnReplace,\n\t\tregexMatch:            opt.regexMatch,\n\t\trulesWithActiveAlerts: opt.rulesWithActiveAlerts,\n\t\tlogger:                log.Default(),\n\t\tparserOpts:            opt.parserOptions,\n\t}\n\tmux := newStrictMux(newInstrumentedMux(http.NewServeMux(), opt.registerer))\n\n\terrs := merrors.New(\n\t\tmux.Handle(\"/federate\", r.el.ExtractLabel(enforceMethods(r.matcher, \"GET\"))),\n\t\tmux.Handle(\"/api/v1/query\", r.el.ExtractLabel(enforceMethods(r.query, \"GET\", \"POST\"))),\n\t\tmux.Handle(\"/api/v1/query_range\", r.el.ExtractLabel(enforceMethods(r.query, \"GET\", \"POST\"))),\n\t\tmux.Handle(\"/api/v1/alerts\", r.el.ExtractLabel(enforceMethods(r.passthrough, \"GET\"))),\n\t\tmux.Handle(\"/api/v1/series\", r.el.ExtractLabel(enforceMethods(r.matcher, \"GET\", \"POST\"))),\n\t\tmux.Handle(\"/api/v1/query_exemplars\", r.el.ExtractLabel(enforceMethods(r.query, \"GET\", \"POST\"))),\n\t)\n\n\tif opt.labelMatchersForRulesAPI {\n\t\terrs.Add(mux.Handle(\"/api/v1/rules\", r.el.ExtractLabel(enforceMethods(r.matcher, \"GET\"))))\n\t} else {\n\t\terrs.Add(mux.Handle(\"/api/v1/rules\", r.el.ExtractLabel(enforceMethods(r.passthrough, \"GET\"))))\n\t}\n\n\tif opt.enableLabelAPIs {\n\t\terrs.Add(\n\t\t\tmux.Handle(\"/api/v1/labels\", r.el.ExtractLabel(enforceMethods(r.matcher, \"GET\", \"POST\"))),\n\t\t\t// Full path is /api/v1/label/<label_name>/values but http mux does not support patterns.\n\t\t\t// This is fine though as we don't care about name for matcher injector.\n\t\t\tmux.Handle(\"/api/v1/label/\", r.el.ExtractLabel(enforceMethods(r.matcher, \"GET\"))),\n\t\t)\n\t}\n\n\terrs.Add(\n\t\t// Reject multi label values with assertSingleLabelValue() because the\n\t\t// semantics of the Silences API don't support multi-label matchers.\n\t\tmux.Handle(\"/api/v2/silences\", r.el.ExtractLabel(\n\t\t\tr.errorIfRegexpMatch(\n\t\t\t\tenforceMethods(\n\t\t\t\t\tassertSingleLabelValue(r.silences),\n\t\t\t\t\t\"GET\", \"POST\",\n\t\t\t\t),\n\t\t\t),\n\t\t)),\n\t\tmux.Handle(\"/api/v2/silence/\", r.el.ExtractLabel(\n\t\t\tr.errorIfRegexpMatch(\n\t\t\t\tenforceMethods(\n\t\t\t\t\tassertSingleLabelValue(r.deleteSilence),\n\t\t\t\t\t\"DELETE\",\n\t\t\t\t),\n\t\t\t),\n\t\t)),\n\t\tmux.Handle(\"/api/v2/alerts/groups\", r.el.ExtractLabel(enforceMethods(r.enforceFilterParameter, \"GET\"))),\n\t\tmux.Handle(\"/api/v2/alerts\", r.el.ExtractLabel(enforceMethods(r.alerts, \"GET\"))),\n\t)\n\n\terrs.Add(\n\t\tmux.Handle(\"/healthz\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t_ = json.NewEncoder(w).Encode(map[string]bool{\"ok\": true})\n\t\t})),\n\t)\n\n\tif err := errs.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Validate paths.\n\tfor _, path := range opt.passthroughPaths {\n\t\tu, err := url.Parse(fmt.Sprintf(\"http://example.com%v\", path))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"path %q is not a valid URI path, got %v\", path, opt.passthroughPaths)\n\t\t}\n\t\tif u.Path != path {\n\t\t\treturn nil, fmt.Errorf(\"path %q is not a valid URI path, got %v\", path, opt.passthroughPaths)\n\t\t}\n\t\tif u.Path == \"\" || u.Path == \"/\" {\n\t\t\treturn nil, fmt.Errorf(\"path %q is not allowed, got %v\", u.Path, opt.passthroughPaths)\n\t\t}\n\t}\n\n\t// Register optional passthrough paths.\n\tfor _, path := range opt.passthroughPaths {\n\t\tif err := mux.Handle(path, http.HandlerFunc(r.passthrough)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tr.mux = mux\n\tr.modifiers = map[string]func(*http.Response) error{\n\t\t\"/api/v1/alerts\": modifyAPIResponse(r.filterAlerts),\n\t}\n\tif !opt.labelMatchersForRulesAPI {\n\t\tr.modifiers[\"/api/v1/rules\"] = modifyAPIResponse(r.filterRules)\n\t}\n\n\t// Configure tls for proxy\n\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\ttransport.TLSClientConfig = &tls.Config{\n\t\tInsecureSkipVerify: opt.insecureSkipVerify,\n\t}\n\n\tif opt.upstreamCaCert != \"\" {\n\t\tcaCert, err := os.ReadFile(opt.upstreamCaCert)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read CA certificate: %v\", err)\n\t\t}\n\n\t\tcaCertPool := x509.NewCertPool()\n\t\tif ok := caCertPool.AppendCertsFromPEM(caCert); !ok {\n\t\t\treturn nil, fmt.Errorf(\"failed to append CA cert to pool\")\n\t\t}\n\n\t\ttransport.TLSClientConfig.RootCAs = caCertPool\n\t}\n\n\tproxy.Transport = transport\n\tproxy.ModifyResponse = r.ModifyResponse\n\tproxy.ErrorHandler = r.errorHandler\n\tproxy.ErrorLog = log.Default()\n\n\treturn r, nil\n}\n\nfunc (r *routes) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tr.mux.ServeHTTP(w, req)\n}\n\nfunc (r *routes) ModifyResponse(resp *http.Response) error {\n\tm, found := r.modifiers[resp.Request.URL.Path]\n\tif !found {\n\t\t// Return the server's response unmodified.\n\t\treturn nil\n\t}\n\n\treturn m(resp)\n}\n\nfunc (r *routes) errorHandler(rw http.ResponseWriter, _ *http.Request, err error) {\n\tr.logger.Printf(\"http: proxy error: %v\", err)\n\tif errors.Is(err, errModifyResponseFailed) {\n\t\trw.WriteHeader(http.StatusBadRequest)\n\t}\n\n\trw.WriteHeader(http.StatusBadGateway)\n}\n\nfunc enforceMethods(h http.HandlerFunc, methods ...string) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, req *http.Request) {\n\t\tif slices.Contains(methods, req.Method) {\n\t\t\th(w, req)\n\t\t\treturn\n\t\t}\n\t\thttp.NotFound(w, req)\n\t}\n}\n\nfunc (r *routes) errorIfRegexpMatch(next http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, req *http.Request) {\n\t\tif r.regexMatch {\n\t\t\tprometheusAPIError(w, \"support for regex match not implemented\", http.StatusNotImplemented)\n\t\t\treturn\n\t\t}\n\n\t\tnext(w, req)\n\t}\n}\n\ntype ctxKey int\n\nconst keyLabel ctxKey = iota\n\n// MustLabelValues returns labels (previously stored using WithLabelValue())\n// from the given context.\n// It will panic if no label is found or the value is empty.\nfunc MustLabelValues(ctx context.Context) []string {\n\tlabels, ok := ctx.Value(keyLabel).([]string)\n\tif !ok {\n\t\tpanic(fmt.Sprintf(\"can't find the %q value in the context\", keyLabel))\n\t}\n\tif len(labels) == 0 {\n\t\tpanic(fmt.Sprintf(\"empty %q value in the context\", keyLabel))\n\t}\n\n\tsort.Strings(labels)\n\n\treturn labels\n}\n\n// MustLabelValue returns the first (alphabetical order) label value previously\n// stored using WithLabelValue() from the given context.\n// Similar to MustLabelValues, it will panic if no label is found or the value\n// is empty.\nfunc MustLabelValue(ctx context.Context) string {\n\tv := MustLabelValues(ctx)\n\treturn v[0]\n}\n\nfunc labelValuesToRegexpString(labelValues []string) string {\n\tlvs := make([]string, len(labelValues))\n\tfor i := range labelValues {\n\t\tlvs[i] = regexp.QuoteMeta(labelValues[i])\n\t}\n\n\treturn strings.Join(lvs, \"|\")\n}\n\n// WithLabelValues stores labels in the given context.\nfunc WithLabelValues(ctx context.Context, labels []string) context.Context {\n\treturn context.WithValue(ctx, keyLabel, labels)\n}\n\nfunc (r *routes) passthrough(w http.ResponseWriter, req *http.Request) {\n\tr.handler.ServeHTTP(w, req)\n}\n\nfunc (r *routes) query(w http.ResponseWriter, req *http.Request) {\n\tmatcher, err := r.newLabelMatcher(MustLabelValues(req.Context())...)\n\tif err != nil {\n\t\tprometheusAPIError(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\te := NewPromQLEnforcerWithOptions(r.errorOnReplace, r.parserOpts, matcher)\n\n\t// The `query` can come in the URL query string and/or the POST body.\n\t// For this reason, we need to try to enforcing in both places.\n\t// Note: a POST request may include some values in the URL query string\n\t// and others in the body. If both locations include a `query`, then\n\t// enforce in both places.\n\tq, found1, err := enforceQueryValues(e, req.URL.Query())\n\tif err != nil {\n\t\tswitch {\n\t\tcase errors.Is(err, ErrIllegalLabelMatcher):\n\t\t\tprometheusAPIError(w, err.Error(), http.StatusBadRequest)\n\t\tcase errors.Is(err, ErrQueryParse):\n\t\t\tprometheusAPIError(w, err.Error(), http.StatusBadRequest)\n\t\tcase errors.Is(err, ErrEnforceLabel):\n\t\t\tprometheusAPIError(w, err.Error(), http.StatusInternalServerError)\n\t\t}\n\n\t\treturn\n\t}\n\treq.URL.RawQuery = q\n\n\tvar found2 bool\n\t// Enforce the query in the POST body if needed.\n\tif req.Method == http.MethodPost {\n\t\tif err := req.ParseForm(); err != nil {\n\t\t\tprometheusAPIError(w, err.Error(), http.StatusBadRequest)\n\t\t}\n\t\tq, found2, err = enforceQueryValues(e, req.PostForm)\n\t\tif err != nil {\n\t\t\tswitch {\n\t\t\tcase errors.Is(err, ErrIllegalLabelMatcher):\n\t\t\t\tprometheusAPIError(w, err.Error(), http.StatusBadRequest)\n\t\t\tcase errors.Is(err, ErrQueryParse):\n\t\t\t\tprometheusAPIError(w, err.Error(), http.StatusBadRequest)\n\t\t\tcase errors.Is(err, ErrEnforceLabel):\n\t\t\t\tprometheusAPIError(w, err.Error(), http.StatusInternalServerError)\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\t// We are replacing request body, close previous one (ParseForm ensures it is read fully and not nil).\n\t\t_ = req.Body.Close()\n\t\treq.Body = io.NopCloser(strings.NewReader(q))\n\t\treq.ContentLength = int64(len(q))\n\t}\n\n\t// If no query was found, return early.\n\tif !found1 && !found2 {\n\t\treturn\n\t}\n\n\tr.handler.ServeHTTP(w, req)\n}\n\nfunc enforceQueryValues(e *PromQLEnforcer, v url.Values) (values string, noQuery bool, err error) {\n\t// If no values were given or no query is present,\n\t// e.g. because the query came in the POST body\n\t// but the URL query string was passed, then finish early.\n\tif v.Get(queryParam) == \"\" {\n\t\treturn v.Encode(), false, nil\n\t}\n\n\tq, err := e.Enforce(v.Get(queryParam))\n\tif err != nil {\n\t\treturn \"\", true, err\n\t}\n\n\tv.Set(queryParam, q)\n\n\treturn v.Encode(), true, nil\n}\n\nfunc (r *routes) newLabelMatcher(vals ...string) (*labels.Matcher, error) {\n\tif r.regexMatch {\n\t\tif len(vals) != 1 {\n\t\t\treturn nil, errors.New(\"only one label value allowed with regex match\")\n\t\t}\n\n\t\tre := vals[0]\n\t\tcompiledRegex, err := regexp.Compile(re)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid regex: %w\", err)\n\t\t}\n\n\t\tif compiledRegex.MatchString(\"\") {\n\t\t\treturn nil, errors.New(\"regex should not match empty string\")\n\t\t}\n\n\t\treturn labels.NewMatcher(labels.MatchRegexp, r.label, re)\n\t}\n\n\tif len(vals) == 1 {\n\t\treturn labels.NewMatcher(\n\t\t\tlabels.MatchEqual,\n\t\t\tr.label,\n\t\t\tvals[0],\n\t\t)\n\t}\n\n\treturn labels.NewMatcher(labels.MatchRegexp, r.label, labelValuesToRegexpString(vals))\n}\n\n// matcher modifies all the match[] HTTP parameters to match on the tenant label.\n// If none was provided, a tenant label matcher matcher is injected.\n// This works for non-query Prometheus API endpoints like /api/v1/series,\n// /api/v1/label/<name>/values, /api/v1/labels and /federate which support\n// multiple matchers.\n// See e.g https://prometheus.io/docs/prometheus/latest/querying/api/#querying-metadata\nfunc (r *routes) matcher(w http.ResponseWriter, req *http.Request) {\n\tmatcher, err := r.newLabelMatcher(MustLabelValues(req.Context())...)\n\tif err != nil {\n\t\tprometheusAPIError(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tq := req.URL.Query()\n\tif err := r.injectMatcher(q, matcher); err != nil {\n\t\tprometheusAPIError(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\treq.URL.RawQuery = q.Encode()\n\tif req.Method == http.MethodPost {\n\t\tif err := req.ParseForm(); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tq = req.PostForm\n\t\tif err := r.injectMatcher(q, matcher); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\t// We are replacing request body, close previous one (ParseForm ensures it is read fully and not nil).\n\t\t_ = req.Body.Close()\n\t\tnewBody := q.Encode()\n\t\treq.Body = io.NopCloser(strings.NewReader(newBody))\n\t\treq.ContentLength = int64(len(newBody))\n\t}\n\n\tr.handler.ServeHTTP(w, req)\n}\n\nfunc (r *routes) injectMatcher(q url.Values, matcher *labels.Matcher) error {\n\tmatchers := q[matchersParam]\n\tif len(matchers) == 0 {\n\t\tq.Set(matchersParam, matchersToString(matcher))\n\t\treturn nil\n\t}\n\n\t// Inject label into existing matchers.\n\tp := parser.NewParser(r.parserOpts)\n\tfor i, m := range matchers {\n\t\tms, err := p.ParseMetricSelector(m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tmatchers[i] = matchersToString(append(ms, matcher)...)\n\t}\n\tq[matchersParam] = matchers\n\n\treturn nil\n}\n\nfunc matchersToString(ms ...*labels.Matcher) string {\n\tvar el []string\n\tfor _, m := range ms {\n\t\tel = append(el, m.String())\n\t}\n\treturn fmt.Sprintf(\"{%v}\", strings.Join(el, \",\"))\n}\n\n// humanFriendlyErrorMessage returns an error message with a capitalized first letter\n// and a punctuation at the end.\nfunc humanFriendlyErrorMessage(err error) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\terrMsg := err.Error()\n\treturn fmt.Sprintf(\"%s%s.\", strings.ToUpper(errMsg[:1]), errMsg[1:])\n}\n\nfunc splitValues(slice []string, sep string) []string {\n\tfor i := 0; i < len(slice); {\n\t\tsplitResult := strings.Split(slice[i], sep)\n\n\t\tslice = append(slice[:i], append(splitResult, slice[i+1:]...)...)\n\n\t\ti += len(splitResult)\n\t}\n\n\treturn slice\n}\n\nfunc removeEmptyValues(slice []string) []string {\n\tfor i := 0; i < len(slice); i++ {\n\t\tif slice[i] == \"\" {\n\t\t\tslice = append(slice[:i], slice[i+1:]...)\n\t\t\ti--\n\t\t}\n\t}\n\n\treturn slice\n}\n\nfunc trimValues(slice []string) []string {\n\tfor i := range slice {\n\t\tslice[i] = strings.TrimSpace(slice[i])\n\t}\n\n\treturn slice\n}\n"
  },
  {
    "path": "injectproxy/routes_test.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage injectproxy\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar okResponse = []byte(`ok`)\n\nfunc checkParameterAbsent(param string, next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\tkvs, err := url.ParseQuery(req.URL.RawQuery)\n\t\tif err != nil {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"unexpected error: %v\", err), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif len(kvs[param]) != 0 {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"unexpected parameter %q\", param), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tnext.ServeHTTP(w, req)\n\t})\n}\n\nfunc checkFormParameterAbsent(param string, next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\terr := req.ParseForm()\n\t\tif err != nil {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"unexpected error: %v\", err), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tkvs := req.Form\n\t\tif len(kvs[param]) != 0 {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"unexpected Form parameter %q\", param), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tnext.ServeHTTP(w, req)\n\t})\n}\n\n// checkQueryHandler verifies that the request form contains the given parameter key/values.\nfunc checkQueryHandler(body, key string, values ...string) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\tkvs, err := url.ParseQuery(req.URL.RawQuery)\n\t\tif err != nil {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"unexpected error: %v\", err), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\t// Verify that the client provides the parameter only once.\n\t\tif len(kvs[key]) != len(values) {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"expected %d values of parameter %q, got %d\", len(values), key, len(kvs[key])), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tsort.Strings(values)\n\t\tsort.Strings(kvs[key])\n\t\tfor i := range values {\n\t\t\tif kvs[key][i] != values[i] {\n\t\t\t\tprometheusAPIError(w, fmt.Sprintf(\"expected parameter %q with value %q, got %q\", key, values[i], kvs[key][i]), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tbuf, err := io.ReadAll(req.Body)\n\t\tif err != nil {\n\t\t\tprometheusAPIError(w, \"failed to read body\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif string(buf) != body {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"expected body %q, got %q\", body, string(buf)), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tw.Write(okResponse)\n\t\t<-time.After(100)\n\t})\n}\n\n// checkFormHandler verifies that the request Form contains the given parameter key/values.\nfunc checkFormHandler(key string, values ...string) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\terr := req.ParseForm()\n\t\tif err != nil {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"unexpected error: %v\", err), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tkvs := req.PostForm\n\t\t// Verify that the client provides the parameter only once.\n\t\tif len(kvs[key]) != len(values) {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"expected %d values of parameter %q, got %d\", len(values), key, len(kvs[key])), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tsort.Strings(values)\n\t\tsort.Strings(kvs[key])\n\t\tfor i := range values {\n\t\t\tif kvs[key][i] != values[i] {\n\t\t\t\tprometheusAPIError(w, fmt.Sprintf(\"expected parameter %q with value %q, got %q\", key, values[i], kvs[key][i]), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tw.Write(okResponse)\n\t\t<-time.After(100)\n\t})\n}\n\n// mockUpstream simulates an upstream HTTP server. It runs on localhost.\ntype mockUpstream struct {\n\th   http.Handler\n\tsrv *httptest.Server\n\turl *url.URL\n}\n\nfunc newMockUpstream(h http.Handler) *mockUpstream {\n\tm := mockUpstream{h: h}\n\n\tm.srv = httptest.NewServer(&m)\n\n\tu, err := url.Parse(m.srv.URL)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tm.url = u\n\n\treturn &m\n}\n\nfunc (m *mockUpstream) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tm.h.ServeHTTP(w, req)\n}\n\nfunc (m *mockUpstream) Close() {\n\tm.srv.Close()\n}\n\nconst proxyLabel = \"namespace\"\n\nfunc TestWithPassthroughPaths(t *testing.T) {\n\tm := newMockUpstream(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Write(okResponse) }))\n\tdefer m.Close()\n\n\tt.Run(\"invalid passthrough options\", func(t *testing.T) {\n\t\t// Duplicated /api.\n\t\t_, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{\"/api1\", \"/api2/something\", \"/api1\"}))\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t\t// Wrong format, params in path.\n\t\t_, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{\"/api1?args=1\", \"/api1\"}))\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t\t// / is not allowed.\n\t\t_, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{\"/\", \"/api2/something\", \"/api1\"}))\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t\t// \"\" is not allowed.\n\t\t_, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{\"/api1\", \"/api2/something\", \"\", \"/api3\"}))\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t\t// Duplication with existing enforced path is not allowed.\n\t\t_, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{\"/api1\", \"/api2/something\", \"/federate\", \"/api3\"}))\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t\t// Duplication with existing enforced path is not allowed.\n\t\t_, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{\"/api1\", \"/api2/something\", \"/federate/\", \"/api3\"}))\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t\t// Duplication with existing enforced path is not allowed.\n\t\t_, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{\"/api1\", \"/api2/something\", \"/federate/some\", \"/api3\"}))\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t\t// api4 is not valid URL path (does not start with /)\n\t\t_, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{\"/api1\", \"/api2/something\", \"api4\", \"/api3\"}))\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t\t// api4/ is not valid URL path (does not start with /)\n\t\t_, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{\"/api1\", \"/api2/something\", \"api4/\", \"/api3\"}))\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t\t// api4/something is not valid URL path (does not start with /)\n\t\t_, err = NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{\"/api1\", \"/api2/something\", \"api4/something\", \"/api3\"}))\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error\")\n\t\t}\n\t})\n\tr, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithPassthroughPaths([]string{\"/api1\", \"/api2/something\", \"/graph/\"}))\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\n\tfor _, tcase := range []struct {\n\t\turl     string\n\t\tmethod  string\n\t\texpCode int\n\t}{\n\t\t{\n\t\t\turl: \"http://prometheus.example.com/graph?namespace=ns1\", method: http.MethodGet,\n\t\t\texpCode: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\turl: \"http://prometheus.example.com/graph\", method: http.MethodPost,\n\t\t\texpCode: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\turl: \"http://prometheus.example.com/graph2\", method: http.MethodPost,\n\t\t\texpCode: http.StatusNotFound,\n\t\t},\n\t\t{\n\t\t\turl: \"http://prometheus.example.com/api/v2/silence\", method: http.MethodGet,\n\t\t\texpCode: http.StatusBadRequest, // Missing label to inject.\n\t\t},\n\t\t{\n\t\t\turl: \"http://prometheus.example.com/api1?yolo=ns1\", method: http.MethodGet,\n\t\t\texpCode: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\turl: \"http://prometheus.example.com/api2/something\", method: http.MethodGet,\n\t\t\texpCode: http.StatusOK,\n\t\t},\n\t} {\n\t\tt.Run(tcase.url, func(t *testing.T) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\tr.ServeHTTP(w, httptest.NewRequest(tcase.method, tcase.url, nil))\n\t\t\tresp := w.Result()\n\t\t\tif resp.StatusCode != tcase.expCode {\n\t\t\t\tb, err := io.ReadAll(resp.Body)\n\t\t\t\tfmt.Println(string(b), err)\n\t\t\t\tt.Fatalf(\"expected status code %v, got %d\", tcase.expCode, resp.StatusCode)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMatch(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tlabelv  []string\n\t\tmatches []string\n\t\topts    []Option\n\n\t\texpCode  int\n\t\texpMatch []string\n\t\texpBody  []byte\n\t}{\n\t\t{\n\t\t\t// No \"namespace\" parameter returns an error.\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\t// No \"match\" parameter.\n\t\t\tlabelv:   []string{\"default\"},\n\t\t\texpCode:  http.StatusOK,\n\t\t\texpMatch: []string{`{namespace=\"default\"}`},\n\t\t\texpBody:  okResponse,\n\t\t},\n\t\t{\n\t\t\t// Single \"match\" parameters.\n\t\t\tlabelv:   []string{\"default\"},\n\t\t\tmatches:  []string{`{job=\"prometheus\",__name__=~\"job:.*\"}`},\n\t\t\texpCode:  http.StatusOK,\n\t\t\texpMatch: []string{`{job=\"prometheus\",__name__=~\"job:.*\",namespace=\"default\"}`},\n\t\t\texpBody:  okResponse,\n\t\t},\n\t\t{\n\t\t\t// Single \"match\" parameters with multiple label values.\n\t\t\tlabelv:   []string{\"default\", \"something\"},\n\t\t\tmatches:  []string{`{job=\"prometheus\",__name__=~\"job:.*\"}`},\n\t\t\texpCode:  http.StatusOK,\n\t\t\texpMatch: []string{`{job=\"prometheus\",__name__=~\"job:.*\",namespace=~\"default|something\"}`},\n\t\t\texpBody:  okResponse,\n\t\t},\n\t\t{\n\t\t\t// Check that label values are correctly escaped.\n\t\t\tlabelv:   []string{\"default\", \"some|thing\"},\n\t\t\tmatches:  []string{`{job=\"prometheus\",__name__=~\"job:.*\"}`},\n\t\t\texpCode:  http.StatusOK,\n\t\t\texpMatch: []string{`{job=\"prometheus\",__name__=~\"job:.*\",namespace=~\"default|some\\\\|thing\"}`},\n\t\t\texpBody:  okResponse,\n\t\t},\n\t\t{\n\t\t\t// Single \"match\" parameters with label dup name.\n\t\t\tlabelv:   []string{\"default\"},\n\t\t\tmatches:  []string{`{job=\"prometheus\",__name__=~\"job:.*\",namespace=\"default\"}`},\n\t\t\texpCode:  http.StatusOK,\n\t\t\texpMatch: []string{`{job=\"prometheus\",__name__=~\"job:.*\",namespace=\"default\",namespace=\"default\"}`},\n\t\t\texpBody:  okResponse,\n\t\t},\n\t\t{\n\t\t\t// Many \"match\" parameters.\n\t\t\tlabelv:   []string{\"default\"},\n\t\t\tmatches:  []string{`{job=\"prometheus\"}`, `{__name__=~\"job:.*\"}`},\n\t\t\texpCode:  http.StatusOK,\n\t\t\texpMatch: []string{`{job=\"prometheus\",namespace=\"default\"}`, `{__name__=~\"job:.*\",namespace=\"default\"}`},\n\t\t\texpBody:  okResponse,\n\t\t},\n\t\t{\n\t\t\t// Many \"match\" parameters with multiple label values.\n\t\t\tlabelv: []string{\"default\", \"something\"},\n\t\t\tmatches: []string{\n\t\t\t\t`{job=\"prometheus\"}`,\n\t\t\t\t`{__name__=~\"job:.*\"}`,\n\t\t\t\t`{namespace=\"something\"}`,\n\t\t\t},\n\t\t\texpCode: http.StatusOK,\n\t\t\texpMatch: []string{\n\t\t\t\t`{job=\"prometheus\",namespace=~\"default|something\"}`,\n\t\t\t\t`{__name__=~\"job:.*\",namespace=~\"default|something\"}`,\n\t\t\t\t`{namespace=\"something\",namespace=~\"default|something\"}`,\n\t\t\t},\n\t\t\texpBody: okResponse,\n\t\t},\n\t\t{\n\t\t\t// Many \"match\" parameters with a single regex value.\n\t\t\tlabelv: []string{\".+-monitoring\"},\n\t\t\tmatches: []string{\n\t\t\t\t`{job=\"prometheus\"}`,\n\t\t\t\t`{__name__=~\"job:.*\"}`,\n\t\t\t\t`{namespace=\"something\"}`,\n\t\t\t},\n\t\t\topts: []Option{WithRegexMatch()},\n\n\t\t\texpCode: http.StatusOK,\n\t\t\texpMatch: []string{\n\t\t\t\t`{job=\"prometheus\",namespace=~\".+-monitoring\"}`,\n\t\t\t\t`{__name__=~\"job:.*\",namespace=~\".+-monitoring\"}`,\n\t\t\t\t`{namespace=\"something\",namespace=~\".+-monitoring\"}`,\n\t\t\t},\n\t\t\texpBody: okResponse,\n\t\t},\n\t\t{\n\t\t\t// A single \"match\" parameter with multiple regex values.\n\t\t\tlabelv: []string{\"default\", \"something\"},\n\t\t\tmatches: []string{\n\t\t\t\t`{job=\"prometheus\"}`,\n\t\t\t},\n\t\t\topts:    []Option{WithRegexMatch()},\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\t// A single \"match\" parameter with a regex value matching the empty string.\n\t\t\tlabelv: []string{\".*\"},\n\t\t\tmatches: []string{\n\t\t\t\t`{job=\"prometheus\"}`,\n\t\t\t},\n\t\t\topts:    []Option{WithRegexMatch()},\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t} {\n\t\tfor _, u := range []string{\n\t\t\t\"http://prometheus.example.com/federate\",\n\t\t\t\"http://prometheus.example.com/api/v1/labels\",\n\t\t\t\"http://prometheus.example.com/api/v1/label/some_label/values\",\n\t\t} {\n\t\t\tt.Run(fmt.Sprintf(\"%s?match[]=%s\", u, strings.Join(tc.matches, \"&\")), func(t *testing.T) {\n\t\t\t\tm := newMockUpstream(\n\t\t\t\t\tcheckParameterAbsent(\n\t\t\t\t\t\tproxyLabel,\n\t\t\t\t\t\tcheckQueryHandler(\"\", matchersParam, tc.expMatch...),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tdefer m.Close()\n\n\t\t\t\tr, err := NewRoutes(\n\t\t\t\t\tm.url,\n\t\t\t\t\tproxyLabel,\n\t\t\t\t\tHTTPFormEnforcer{ParameterName: proxyLabel},\n\t\t\t\t\tappend([]Option{WithEnabledLabelsAPI()}, tc.opts...)...,\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tu, err := url.Parse(u)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t\tq := u.Query()\n\t\t\t\tfor _, m := range tc.matches {\n\t\t\t\t\tq.Add(matchersParam, m)\n\t\t\t\t}\n\t\t\t\tfor _, lv := range tc.labelv {\n\t\t\t\t\tq.Add(proxyLabel, lv)\n\t\t\t\t}\n\t\t\t\tu.RawQuery = q.Encode()\n\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"GET\", u.String(), nil)\n\t\t\t\tr.ServeHTTP(w, req)\n\n\t\t\t\tresp := w.Result()\n\t\t\t\tbody, _ := io.ReadAll(resp.Body)\n\t\t\t\tdefer resp.Body.Close()\n\n\t\t\t\tif resp.StatusCode != tc.expCode {\n\t\t\t\t\tt.Logf(\"expected status code %d, got %d\", tc.expCode, resp.StatusCode)\n\t\t\t\t\tt.Logf(\"%s\", string(body))\n\t\t\t\t\tt.FailNow()\n\t\t\t\t}\n\n\t\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif string(body) != string(tc.expBody) {\n\t\t\t\t\tt.Fatalf(\"expected body %q, got %q\", string(tc.expBody), string(body))\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestMatchWithPost(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tlabelv  []string\n\t\tmatches []string\n\n\t\texpCode  int\n\t\texpMatch []string\n\t\texpBody  []byte\n\t}{\n\t\t{\n\t\t\t// No \"namespace\" parameter returns an error.\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\t// No \"match\" parameter.\n\t\t\tlabelv:   []string{\"default\"},\n\t\t\texpCode:  http.StatusOK,\n\t\t\texpMatch: []string{`{namespace=\"default\"}`},\n\t\t\texpBody:  okResponse,\n\t\t},\n\t\t{\n\t\t\t// Single \"match\" parameters.\n\t\t\tlabelv:   []string{\"default\"},\n\t\t\tmatches:  []string{`{job=\"prometheus\",__name__=~\"job:.*\"}`},\n\t\t\texpCode:  http.StatusOK,\n\t\t\texpMatch: []string{`{job=\"prometheus\",__name__=~\"job:.*\",namespace=\"default\"}`},\n\t\t\texpBody:  okResponse,\n\t\t},\n\t\t{\n\t\t\t// Single \"match\" parameters with multiple label values.\n\t\t\tlabelv:   []string{\"default\", \"something\"},\n\t\t\tmatches:  []string{`{job=\"prometheus\",__name__=~\"job:.*\"}`},\n\t\t\texpCode:  http.StatusOK,\n\t\t\texpMatch: []string{`{job=\"prometheus\",__name__=~\"job:.*\",namespace=~\"default|something\"}`},\n\t\t\texpBody:  okResponse,\n\t\t},\n\t\t{\n\t\t\t// Check that label values are correctly escaped.\n\t\t\tlabelv:   []string{\"default\", \"some|thing\"},\n\t\t\tmatches:  []string{`{job=\"prometheus\",__name__=~\"job:.*\"}`},\n\t\t\texpCode:  http.StatusOK,\n\t\t\texpMatch: []string{`{job=\"prometheus\",__name__=~\"job:.*\",namespace=~\"default|some\\\\|thing\"}`},\n\t\t\texpBody:  okResponse,\n\t\t},\n\t\t{\n\t\t\t// Single \"match\" parameters with label dup name.\n\t\t\tlabelv:   []string{\"default\"},\n\t\t\tmatches:  []string{`{job=\"prometheus\",__name__=~\"job:.*\",namespace=\"default\"}`},\n\t\t\texpCode:  http.StatusOK,\n\t\t\texpMatch: []string{`{job=\"prometheus\",__name__=~\"job:.*\",namespace=\"default\",namespace=\"default\"}`},\n\t\t\texpBody:  okResponse,\n\t\t},\n\t\t{\n\t\t\t// Many \"match\" parameters.\n\t\t\tlabelv:   []string{\"default\"},\n\t\t\tmatches:  []string{`{job=\"prometheus\"}`, `{__name__=~\"job:.*\"}`},\n\t\t\texpCode:  http.StatusOK,\n\t\t\texpMatch: []string{`{job=\"prometheus\",namespace=\"default\"}`, `{__name__=~\"job:.*\",namespace=\"default\"}`},\n\t\t\texpBody:  okResponse,\n\t\t},\n\t\t{\n\t\t\t// Many \"match\" parameters with multiple label values.\n\t\t\tlabelv: []string{\"default\", \"something\"},\n\t\t\tmatches: []string{\n\t\t\t\t`{job=\"prometheus\"}`,\n\t\t\t\t`{__name__=~\"job:.*\"}`,\n\t\t\t\t`{namespace=\"something\"}`,\n\t\t\t},\n\t\t\texpCode: http.StatusOK,\n\t\t\texpMatch: []string{\n\t\t\t\t`{job=\"prometheus\",namespace=~\"default|something\"}`,\n\t\t\t\t`{__name__=~\"job:.*\",namespace=~\"default|something\"}`,\n\t\t\t\t`{namespace=\"something\",namespace=~\"default|something\"}`,\n\t\t\t},\n\t\t\texpBody: okResponse,\n\t\t},\n\t} {\n\t\tfor _, u := range []string{\n\t\t\t\"http://prometheus.example.com/api/v1/labels\",\n\t\t} {\n\t\t\tt.Run(fmt.Sprintf(\"%s?match[]=%s\", u, strings.Join(tc.matches, \"&\")), func(t *testing.T) {\n\t\t\t\tm := newMockUpstream(\n\t\t\t\t\tcheckFormParameterAbsent(\n\t\t\t\t\t\tproxyLabel,\n\t\t\t\t\t\tcheckFormHandler(matchersParam, tc.expMatch...),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tdefer m.Close()\n\t\t\t\tr, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithEnabledLabelsAPI())\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tu, err := url.Parse(u)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t\tq := url.Values{}\n\t\t\t\tfor _, m := range tc.matches {\n\t\t\t\t\tq.Add(matchersParam, m)\n\t\t\t\t}\n\t\t\t\tfor _, lv := range tc.labelv {\n\t\t\t\t\tq.Add(proxyLabel, lv)\n\t\t\t\t}\n\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(http.MethodPost, u.String(), strings.NewReader(q.Encode()))\n\t\t\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\t\t\tr.ServeHTTP(w, req)\n\n\t\t\t\tresp := w.Result()\n\t\t\t\tbody, _ := io.ReadAll(resp.Body)\n\t\t\t\tdefer resp.Body.Close()\n\n\t\t\t\tif resp.StatusCode != tc.expCode {\n\t\t\t\t\tt.Logf(\"expected status code %d, got %d\", tc.expCode, resp.StatusCode)\n\t\t\t\t\tt.Logf(\"%s\", string(body))\n\t\t\t\t\tt.FailNow()\n\t\t\t\t}\n\t\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif string(body) != string(tc.expBody) {\n\t\t\t\t\tt.Fatalf(\"expected body %q, got %q\", string(tc.expBody), string(body))\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestSeries(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tname        string\n\t\tlabelv      []string\n\t\tpromQuery   string\n\t\texpResponse []byte\n\t\texpCode     int\n\t\texpMatch    []string\n\t\texpBody     []byte\n\t}{\n\t\t{\n\t\t\tname:    `No \"namespace\" parameter returns an error`,\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\tname:    `No \"namespace\" parameter returns an error for POSTs`,\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\tname:        `No \"match[]\" parameter returns 200 with empty body`,\n\t\t\tlabelv:      []string{\"default\"},\n\t\t\texpMatch:    []string{`{namespace=\"default\"}`},\n\t\t\texpResponse: okResponse,\n\t\t\texpCode:     http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:        `No \"match[]\" parameter returns 200 with empty body for POSTs`,\n\t\t\tlabelv:      []string{\"default\"},\n\t\t\texpMatch:    []string{`{namespace=\"default\"}`},\n\t\t\texpResponse: okResponse,\n\t\t\texpCode:     http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:        `Series`,\n\t\t\tlabelv:      []string{\"default\"},\n\t\t\tpromQuery:   \"up\",\n\t\t\texpCode:     http.StatusOK,\n\t\t\texpMatch:    []string{`{__name__=\"up\",namespace=\"default\"}`},\n\t\t\texpResponse: okResponse,\n\t\t},\n\t\t{\n\t\t\tname:        `Series with multiple label values`,\n\t\t\tlabelv:      []string{\"default\", \"something\"},\n\t\t\tpromQuery:   \"up\",\n\t\t\texpCode:     http.StatusOK,\n\t\t\texpMatch:    []string{`{__name__=\"up\",namespace=~\"default|something\"}`},\n\t\t\texpResponse: okResponse,\n\t\t},\n\t\t{\n\t\t\tname:        `Series: check that label values are correctly escaped`,\n\t\t\tlabelv:      []string{\"default\", \"some|thing\"},\n\t\t\tpromQuery:   \"up\",\n\t\t\texpCode:     http.StatusOK,\n\t\t\texpMatch:    []string{`{__name__=\"up\",namespace=~\"default|some\\\\|thing\"}`},\n\t\t\texpResponse: okResponse,\n\t\t},\n\t\t{\n\t\t\tname:        `Series with multiple labels`,\n\t\t\tlabelv:      []string{\"default\"},\n\t\t\tpromQuery:   `up{instance=\"localhost:9090\"}`,\n\t\t\texpCode:     http.StatusOK,\n\t\t\texpMatch:    []string{`{instance=\"localhost:9090\",__name__=\"up\",namespace=\"default\"}`},\n\t\t\texpResponse: okResponse,\n\t\t},\n\t\t{\n\t\t\tname:        `Series with multiple label values and existing matcher`,\n\t\t\tlabelv:      []string{\"default\", \"something\"},\n\t\t\tpromQuery:   `up{instance=\"localhost:9090\",namespace=\"something\"}`,\n\t\t\texpCode:     http.StatusOK,\n\t\t\texpMatch:    []string{`{instance=\"localhost:9090\",namespace=\"something\",__name__=\"up\",namespace=~\"default|something\"}`},\n\t\t\texpResponse: okResponse,\n\t\t},\n\t} {\n\t\tfor _, endpoint := range []string{\"series\"} {\n\t\t\tt.Run(endpoint+\"/\"+strings.ReplaceAll(tc.name, \" \", \"_\"), func(t *testing.T) {\n\t\t\t\tm := newMockUpstream(\n\t\t\t\t\tcheckParameterAbsent(\n\t\t\t\t\t\tproxyLabel,\n\t\t\t\t\t\tcheckQueryHandler(\"\", matchersParam, tc.expMatch...),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tdefer m.Close()\n\n\t\t\t\tr, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel})\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tu, err := url.Parse(\"http://prometheus.example.com/api/v1/\" + endpoint)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t\tq := u.Query()\n\t\t\t\tif tc.promQuery != \"\" {\n\t\t\t\t\tq.Add(matchersParam, tc.promQuery)\n\t\t\t\t}\n\t\t\t\tfor _, lv := range tc.labelv {\n\t\t\t\t\tq.Add(proxyLabel, lv)\n\t\t\t\t}\n\t\t\t\tu.RawQuery = q.Encode()\n\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"GET\", u.String(), nil)\n\t\t\t\tr.ServeHTTP(w, req)\n\n\t\t\t\tresp := w.Result()\n\n\t\t\t\tbody, err := io.ReadAll(resp.Body)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t\tdefer resp.Body.Close()\n\n\t\t\t\tif resp.StatusCode != tc.expCode {\n\t\t\t\t\tt.Logf(\"expected status code %d, got %d\", tc.expCode, resp.StatusCode)\n\t\t\t\t\tt.Logf(\"%s\", string(body))\n\t\t\t\t\tt.FailNow()\n\t\t\t\t}\n\t\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif string(body) != string(tc.expResponse) {\n\t\t\t\t\tt.Fatalf(\"expected response body %q, got %q\", string(tc.expResponse), string(body))\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestSeriesWithPost(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tname          string\n\t\tlabelv        []string\n\t\tpromQueryBody string\n\t\texpResponse   []byte\n\t\tmethod        string\n\t\texpCode       int\n\t\texpMatch      []string\n\t\texpBody       []byte\n\t}{\n\t\t{\n\t\t\tname:    `No \"namespace\" parameter returns an error`,\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\tname:    `No \"namespace\" parameter returns an error for POSTs`,\n\t\t\texpCode: http.StatusBadRequest,\n\t\t\tmethod:  http.MethodPost,\n\t\t},\n\t\t{\n\t\t\tname:        `No \"match[]\" parameter returns 200 with empty body`,\n\t\t\tlabelv:      []string{\"default\"},\n\t\t\tmethod:      http.MethodPost,\n\t\t\texpMatch:    []string{`{namespace=\"default\"}`},\n\t\t\texpResponse: okResponse,\n\t\t\texpCode:     http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:        `No \"match[]\" parameter returns 200 with empty body for POSTs`,\n\t\t\tmethod:      http.MethodPost,\n\t\t\tlabelv:      []string{\"default\"},\n\t\t\texpMatch:    []string{`{namespace=\"default\"}`},\n\t\t\texpResponse: okResponse,\n\t\t\texpCode:     http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:          `Series POST`,\n\t\t\tlabelv:        []string{\"default\"},\n\t\t\tpromQueryBody: \"up\",\n\t\t\tmethod:        http.MethodPost,\n\t\t\texpCode:       http.StatusOK,\n\t\t\texpMatch:      []string{`{__name__=\"up\",namespace=\"default\"}`},\n\t\t\texpResponse:   okResponse,\n\t\t},\n\t\t{\n\t\t\tname:          `Series POST with multiple label values`,\n\t\t\tlabelv:        []string{\"default\", \"something\"},\n\t\t\tpromQueryBody: \"up\",\n\t\t\tmethod:        http.MethodPost,\n\t\t\texpCode:       http.StatusOK,\n\t\t\texpMatch:      []string{`{__name__=\"up\",namespace=~\"default|something\"}`},\n\t\t\texpResponse:   okResponse,\n\t\t},\n\t\t{\n\t\t\tname:          `Series POST: check that label values are correctly escaped`,\n\t\t\tlabelv:        []string{\"default\", \"some|thing\"},\n\t\t\tpromQueryBody: \"up\",\n\t\t\tmethod:        http.MethodPost,\n\t\t\texpCode:       http.StatusOK,\n\t\t\texpMatch:      []string{`{__name__=\"up\",namespace=~\"default|some\\\\|thing\"}`},\n\t\t\texpResponse:   okResponse,\n\t\t},\n\t\t{\n\t\t\tname:          `Series with labels POST`,\n\t\t\tlabelv:        []string{\"default\"},\n\t\t\tpromQueryBody: `up{instance=\"localhost:9090\"}`,\n\t\t\tmethod:        http.MethodPost,\n\t\t\texpCode:       http.StatusOK,\n\t\t\texpMatch:      []string{`{instance=\"localhost:9090\",__name__=\"up\",namespace=\"default\"}`},\n\t\t\texpResponse:   okResponse,\n\t\t},\n\t\t{\n\t\t\tname:          `Series POST with multiple label values and existing matcher`,\n\t\t\tlabelv:        []string{\"default\", \"something\"},\n\t\t\tpromQueryBody: `up{instance=\"localhost:9090\",namespace=\"something\"}`,\n\t\t\tmethod:        http.MethodPost,\n\t\t\texpCode:       http.StatusOK,\n\t\t\texpMatch:      []string{`{instance=\"localhost:9090\",namespace=\"something\",__name__=\"up\",namespace=~\"default|something\"}`},\n\t\t\texpResponse:   okResponse,\n\t\t},\n\t} {\n\t\tfor _, endpoint := range []string{\"series\"} {\n\t\t\tt.Run(endpoint+\"/\"+strings.ReplaceAll(tc.name, \" \", \"_\"), func(t *testing.T) {\n\t\t\t\tm := newMockUpstream(\n\t\t\t\t\tcheckParameterAbsent(\n\t\t\t\t\t\tproxyLabel,\n\t\t\t\t\t\tcheckFormHandler(matchersParam, tc.expMatch...),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tdefer m.Close()\n\n\t\t\t\tr, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel})\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tu, err := url.Parse(\"http://prometheus.example.com/api/v1/\" + endpoint)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t\tq := u.Query()\n\t\t\t\tfor _, lv := range tc.labelv {\n\t\t\t\t\tq.Add(proxyLabel, lv)\n\t\t\t\t}\n\t\t\t\tu.RawQuery = q.Encode()\n\n\t\t\t\tvar b io.Reader = nil\n\t\t\t\tif tc.promQueryBody != \"\" {\n\t\t\t\t\tb = strings.NewReader(url.Values(map[string][]string{\"match[]\": {tc.promQueryBody}}).Encode())\n\t\t\t\t}\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(tc.method, u.String(), b)\n\t\t\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\t\t\tr.ServeHTTP(w, req)\n\n\t\t\t\tresp := w.Result()\n\n\t\t\t\tbody, err := io.ReadAll(resp.Body)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t\tdefer resp.Body.Close()\n\n\t\t\t\tif resp.StatusCode != tc.expCode {\n\t\t\t\t\tt.Logf(\"expected status code %d, got %d\", tc.expCode, resp.StatusCode)\n\t\t\t\t\tt.Logf(\"%s\", string(body))\n\t\t\t\t\tt.FailNow()\n\t\t\t\t}\n\t\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif string(body) != string(tc.expResponse) {\n\t\t\t\t\tt.Fatalf(\"expected response body %q, got %q\", string(tc.expResponse), string(body))\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestQuery(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tname           string\n\t\tlabelv         []string\n\t\theaders        http.Header\n\t\theaderName     string\n\t\tqueryParam     string\n\t\tstaticLabelVal []string\n\t\tpromQuery      string\n\t\tpromQueryBody  string\n\t\tmethod         string\n\n\t\texpCode              int\n\t\texpPromQuery         string\n\t\texpPromQueryBody     string\n\t\texpResponse          []byte\n\t\terrorOnReplace       bool\n\t\tregexMatch           bool\n\t\theaderUsesListSyntax bool\n\t}{\n\t\t{\n\t\t\tname:    `No \"namespace\" parameter returns an error`,\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\tname:    `No \"namespace\" parameter returns an error for POSTs`,\n\t\t\texpCode: http.StatusBadRequest,\n\t\t\tmethod:  http.MethodPost,\n\t\t},\n\t\t{\n\t\t\tlabelv:  []string{\"default\", \"\"},\n\t\t\tname:    `One of the \"namespace\" parameters empty returns 200`,\n\t\t\texpCode: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tlabelv:  []string{\"default\", \"\"},\n\t\t\tname:    `One of the \"namespace\" parameters empty returns 200 for POSTs`,\n\t\t\texpCode: http.StatusOK,\n\t\t\tmethod:  http.MethodPost,\n\t\t},\n\t\t{\n\t\t\tname:    `No \"query\" parameter returns 200 with empty body`,\n\t\t\tlabelv:  []string{\"default\"},\n\t\t\texpCode: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:    `No \"query\" parameter returns 200 with empty body for POSTs`,\n\t\t\tlabelv:  []string{\"default\"},\n\t\t\texpCode: http.StatusOK,\n\t\t\tmethod:  http.MethodPost,\n\t\t},\n\t\t{\n\t\t\tname:         `Query without a vector selector`,\n\t\t\tlabelv:       []string{\"default\"},\n\t\t\tpromQuery:    \"up\",\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{namespace=\"default\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `Query: check that label values are correctly escaped`,\n\t\t\tlabelv:       []string{\"de|fault\", \"something\"},\n\t\t\tpromQuery:    \"up\",\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{namespace=~\"de\\\\|fault|something\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `Query: check that label values are not escaped for single label values`,\n\t\t\tlabelv:       []string{\"de|fault\"},\n\t\t\tpromQuery:    \"up\",\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{namespace=\"de|fault\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `Query without a vector selector with multiple label values`,\n\t\t\tlabelv:       []string{\"default\", \"second\"},\n\t\t\tpromQuery:    \"up\",\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{namespace=~\"default|second\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:             `Query without a vector selector in POST body`,\n\t\t\tlabelv:           []string{\"default\"},\n\t\t\tpromQueryBody:    \"up\",\n\t\t\tmethod:           http.MethodPost,\n\t\t\texpCode:          http.StatusOK,\n\t\t\texpPromQueryBody: `up{namespace=\"default\"}`,\n\t\t\texpResponse:      okResponse,\n\t\t},\n\t\t{\n\t\t\tname:             `Query without a vector selector in POST body with multiple label values`,\n\t\t\tlabelv:           []string{\"default\", \"second\"},\n\t\t\tpromQueryBody:    \"up\",\n\t\t\tmethod:           http.MethodPost,\n\t\t\texpCode:          http.StatusOK,\n\t\t\texpPromQueryBody: `up{namespace=~\"default|second\"}`,\n\t\t\texpResponse:      okResponse,\n\t\t},\n\t\t{\n\t\t\tname:             `Tricky: Query without a vector selector in GET body (yes, that's possible)'`,\n\t\t\tlabelv:           []string{\"default\"},\n\t\t\tpromQueryBody:    \"up\",\n\t\t\tmethod:           http.MethodGet,\n\t\t\texpCode:          http.StatusOK,\n\t\t\texpPromQueryBody: ``, // We should finish request without forwarding. Form should not parse this value for GET.\n\t\t},\n\t\t{\n\t\t\tname:             `Query without a vector selector in POST body or query`,\n\t\t\tlabelv:           []string{\"default\"},\n\t\t\tpromQuery:        \"up\",\n\t\t\tpromQueryBody:    \"up\",\n\t\t\tmethod:           http.MethodPost,\n\t\t\texpCode:          http.StatusOK,\n\t\t\texpPromQuery:     `up{namespace=\"default\"}`,\n\t\t\texpPromQueryBody: `up{namespace=\"default\"}`,\n\t\t\texpResponse:      okResponse,\n\t\t},\n\t\t{\n\t\t\tname:             `Query without a vector selector in POST body or query with multiple label values`,\n\t\t\tlabelv:           []string{\"default\", \"second\"},\n\t\t\tpromQuery:        \"up\",\n\t\t\tpromQueryBody:    \"up\",\n\t\t\tmethod:           http.MethodPost,\n\t\t\texpCode:          http.StatusOK,\n\t\t\texpPromQuery:     `up{namespace=~\"default|second\"}`,\n\t\t\texpPromQueryBody: `up{namespace=~\"default|second\"}`,\n\t\t\texpResponse:      okResponse,\n\t\t},\n\t\t{\n\t\t\tname:             `Query without a vector selector in POST body or query different`,\n\t\t\tlabelv:           []string{\"default\"},\n\t\t\tpromQuery:        \"up\",\n\t\t\tpromQueryBody:    \"foo\",\n\t\t\tmethod:           http.MethodPost,\n\t\t\texpCode:          http.StatusOK,\n\t\t\texpPromQuery:     `up{namespace=\"default\"}`,\n\t\t\texpPromQueryBody: `foo{namespace=\"default\"}`,\n\t\t\texpResponse:      okResponse,\n\t\t},\n\t\t{\n\t\t\tname:             `Query without a vector selector in POST body or query different with multiple label values`,\n\t\t\tlabelv:           []string{\"default\", \"second\"},\n\t\t\tpromQuery:        \"up\",\n\t\t\tpromQueryBody:    \"foo\",\n\t\t\tmethod:           http.MethodPost,\n\t\t\texpCode:          http.StatusOK,\n\t\t\texpPromQuery:     `up{namespace=~\"default|second\"}`,\n\t\t\texpPromQueryBody: `foo{namespace=~\"default|second\"}`,\n\t\t\texpResponse:      okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `Query with a vector selector`,\n\t\t\tlabelv:       []string{\"default\"},\n\t\t\tpromQuery:    `up{namespace=\"other\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{namespace=\"default\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `Query with a vector selector with multiple label values`,\n\t\t\tlabelv:       []string{\"default\", \"second\"},\n\t\t\tpromQuery:    `up{namespace=\"second\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{namespace=\"second\",namespace=~\"default|second\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `Query with a vector selector with empty label values`,\n\t\t\tlabelv:       []string{\"default\", \"\"},\n\t\t\tpromQuery:    `up{namespace=\"other\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{namespace=\"default\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:             `Query with a vector selector in POST body`,\n\t\t\tlabelv:           []string{\"default\"},\n\t\t\tpromQueryBody:    `up{namespace=\"other\"}`,\n\t\t\tmethod:           http.MethodPost,\n\t\t\texpCode:          http.StatusOK,\n\t\t\texpPromQueryBody: `up{namespace=\"default\"}`,\n\t\t\texpResponse:      okResponse,\n\t\t},\n\t\t{\n\t\t\tname:             `Query with a vector selector in POST body with multiple label values`,\n\t\t\tlabelv:           []string{\"default\", \"second\"},\n\t\t\tpromQueryBody:    `up{namespace=\"second\"}`,\n\t\t\tmethod:           http.MethodPost,\n\t\t\texpCode:          http.StatusOK,\n\t\t\texpPromQueryBody: `up{namespace=\"second\",namespace=~\"default|second\"}`,\n\t\t\texpResponse:      okResponse,\n\t\t},\n\t\t{\n\t\t\tname:           `Query with a vector selector and errorOnReplace`,\n\t\t\tlabelv:         []string{\"default\"},\n\t\t\tpromQuery:      `up{namespace=\"other\"}`,\n\t\t\terrorOnReplace: true,\n\t\t\texpCode:        http.StatusBadRequest,\n\t\t\texpResponse:    nil,\n\t\t},\n\t\t{\n\t\t\tname:           `Query with a vector selector, multiple values and errorOnReplace`,\n\t\t\tlabelv:         []string{\"default\", \"default2\"},\n\t\t\tpromQuery:      `up{namespace=\"other\"}`,\n\t\t\terrorOnReplace: true,\n\t\t\texpCode:        http.StatusBadRequest,\n\t\t\texpResponse:    nil,\n\t\t},\n\t\t{\n\t\t\tname:           `Query with a vector selector in POST body and errorOnReplace`,\n\t\t\tlabelv:         []string{\"default\"},\n\t\t\tpromQueryBody:  `up{namespace=\"other\"}`,\n\t\t\tmethod:         http.MethodPost,\n\t\t\terrorOnReplace: true,\n\t\t\texpCode:        http.StatusBadRequest,\n\t\t\texpResponse:    nil,\n\t\t},\n\t\t{\n\t\t\tname:         `Query with a scalar`,\n\t\t\tlabelv:       []string{\"default\"},\n\t\t\tpromQuery:    \"1\",\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `1`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:             `Query with a scalar in POST body`,\n\t\t\tlabelv:           []string{\"default\"},\n\t\t\tpromQueryBody:    \"1\",\n\t\t\tmethod:           http.MethodPost,\n\t\t\texpCode:          http.StatusOK,\n\t\t\texpPromQueryBody: `1`,\n\t\t\texpResponse:      okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `Query with a function`,\n\t\t\tlabelv:       []string{\"default\"},\n\t\t\tpromQuery:    \"time()\",\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `time()`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:             `Query with a function in POST body`,\n\t\t\tlabelv:           []string{\"default\"},\n\t\t\tpromQueryBody:    \"time()\",\n\t\t\tmethod:           http.MethodPost,\n\t\t\texpCode:          http.StatusOK,\n\t\t\texpPromQueryBody: `time()`,\n\t\t\texpResponse:      okResponse,\n\t\t},\n\t\t{\n\t\t\tname:      `An invalid expression returns 400 with error response`,\n\t\t\tlabelv:    []string{\"default\"},\n\t\t\tpromQuery: \"up +\",\n\t\t\texpCode:   http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\tname:          `An invalid expression in POST body returns 400 with error response`,\n\t\t\tlabelv:        []string{\"default\"},\n\t\t\tpromQueryBody: \"up +\",\n\t\t\tmethod:        http.MethodPost,\n\t\t\texpCode:       http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\tname:         `Binary expression`,\n\t\t\tlabelv:       []string{\"default\"},\n\t\t\tpromQuery:    `up{instance=\"localhost:9090\"} + foo{namespace=\"other\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{instance=\"localhost:9090\",namespace=\"default\"} + foo{namespace=\"default\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `Binary expression with multiple label values`,\n\t\t\tlabelv:       []string{\"default\", \"second\"},\n\t\t\tpromQuery:    `up{instance=\"localhost:9090\"} + foo{namespace=\"second\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{instance=\"localhost:9090\",namespace=~\"default|second\"} + foo{namespace=\"second\",namespace=~\"default|second\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:           `Static label value`,\n\t\t\tstaticLabelVal: []string{\"default\"},\n\t\t\tpromQuery:      `up{instance=\"localhost:9090\"} + foo{namespace=\"default\"}`,\n\t\t\texpCode:        http.StatusOK,\n\t\t\texpPromQuery:   `up{instance=\"localhost:9090\",namespace=\"default\"} + foo{namespace=\"default\"}`,\n\t\t\texpResponse:    okResponse,\n\t\t},\n\t\t{\n\t\t\tname:           `Multiple static label values`,\n\t\t\tstaticLabelVal: []string{\"default\", \"second\"},\n\t\t\tpromQuery:      `up{instance=\"localhost:9090\"} + foo{namespace=\"second\"}`,\n\t\t\texpCode:        http.StatusOK,\n\t\t\texpPromQuery:   `up{instance=\"localhost:9090\",namespace=~\"default|second\"} + foo{namespace=\"second\",namespace=~\"default|second\"}`,\n\t\t\texpResponse:    okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `HTTP header label value`,\n\t\t\theaders:      http.Header{\"namespace\": []string{\"default\"}},\n\t\t\theaderName:   \"namespace\",\n\t\t\tpromQuery:    `up{instance=\"localhost:9090\"} + foo{namespace=\"other\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{instance=\"localhost:9090\",namespace=\"default\"} + foo{namespace=\"default\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `multiple HTTP header label values`,\n\t\t\theaders:      http.Header{\"namespace\": []string{\"default\", \"second\"}},\n\t\t\theaderName:   \"namespace\",\n\t\t\tpromQuery:    `up{instance=\"localhost:9090\"} + foo{namespace=\"second\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{instance=\"localhost:9090\",namespace=~\"default|second\"} + foo{namespace=\"second\",namespace=~\"default|second\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `HTTP header label with comma-separated values and list parsing disabled`,\n\t\t\theaders:      http.Header{\"namespace\": []string{\"default, second\", \"third\"}},\n\t\t\theaderName:   \"namespace\",\n\t\t\tpromQuery:    `up{instance=\"localhost:9090\"} + foo{namespace=\"second\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{instance=\"localhost:9090\",namespace=~\"default, second|third\"} + foo{namespace=\"second\",namespace=~\"default, second|third\"}`,\n\t\t\texpResponse:  okResponse,\n\n\t\t\theaderUsesListSyntax: false,\n\t\t},\n\t\t{\n\t\t\tname:         `HTTP header label with comma-separated values and list parsing enabled`,\n\t\t\theaders:      http.Header{\"namespace\": []string{\"default, second\", \"third\"}},\n\t\t\theaderName:   \"namespace\",\n\t\t\tpromQuery:    `up{instance=\"localhost:9090\"} + foo{namespace=\"second\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{instance=\"localhost:9090\",namespace=~\"default|second|third\"} + foo{namespace=\"second\",namespace=~\"default|second|third\"}`,\n\t\t\texpResponse:  okResponse,\n\n\t\t\theaderUsesListSyntax: true,\n\t\t},\n\t\t{\n\t\t\tname:         `multiple HTTP header with empty label value`,\n\t\t\theaders:      http.Header{\"namespace\": []string{\"default\", \"\"}},\n\t\t\theaderName:   \"namespace\",\n\t\t\tpromQuery:    `up{instance=\"localhost:9090\"} + foo{namespace=\"other\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{instance=\"localhost:9090\",namespace=\"default\"} + foo{namespace=\"default\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `query param label value`,\n\t\t\tqueryParam:   \"namespace2\",\n\t\t\tlabelv:       []string{\"default\"},\n\t\t\tpromQuery:    `up{instance=\"localhost:9090\"} + foo{namespace=\"other\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{instance=\"localhost:9090\",namespace=\"default\"} + foo{namespace=\"default\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `HTTP header as regexp`,\n\t\t\theaders:      http.Header{\"namespace\": []string{\"tenant1-.*\"}},\n\t\t\theaderName:   \"namespace\",\n\t\t\tregexMatch:   true,\n\t\t\tpromQuery:    `up{instance=\"localhost:9090\"} + foo{namespace=\"other\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{instance=\"localhost:9090\",namespace=~\"tenant1-.*\"} + foo{namespace=\"other\",namespace=~\"tenant1-.*\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `query param as regexp`,\n\t\t\tqueryParam:   \"namespace\",\n\t\t\tlabelv:       []string{\"tenant1-.*\"},\n\t\t\tregexMatch:   true,\n\t\t\tpromQuery:    `up{instance=\"localhost:9090\"} + foo{namespace=\"other\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{instance=\"localhost:9090\",namespace=~\"tenant1-.*\"} + foo{namespace=\"other\",namespace=~\"tenant1-.*\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:         `HTTP header as regexp with same regexp in query`,\n\t\t\theaders:      http.Header{\"namespace\": []string{\"tenant1-.*\"}},\n\t\t\theaderName:   \"namespace\",\n\t\t\tregexMatch:   true,\n\t\t\tpromQuery:    `up{instance=\"localhost:9090\"} + foo{namespace=\"tenant1-.*\"}`,\n\t\t\texpCode:      http.StatusOK,\n\t\t\texpPromQuery: `up{instance=\"localhost:9090\",namespace=~\"tenant1-.*\"} + foo{namespace=\"tenant1-.*\",namespace=~\"tenant1-.*\"}`,\n\t\t\texpResponse:  okResponse,\n\t\t},\n\t\t{\n\t\t\tname:       `HTTP header with invalid regexp with same regexp in query`,\n\t\t\theaders:    http.Header{\"namespace\": []string{\"tenant1-(.*\"}},\n\t\t\theaderName: \"namespace\",\n\t\t\tregexMatch: true,\n\t\t\tpromQuery:  `up{instance=\"localhost:9090\"} + foo{namespace=\"tenant1-.*\"}`,\n\t\t\texpCode:    http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\tname:       `Multiple regexp HTTP headers is invalid`,\n\t\t\theaders:    http.Header{\"namespace\": []string{\"tenant1\", \"tenant2\"}},\n\t\t\theaderName: \"namespace\",\n\t\t\tregexMatch: true,\n\t\t\tpromQuery:  `up{instance=\"localhost:9090\"} + foo{namespace=\"tenant1-.*\"}`,\n\t\t\texpCode:    http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\tname:       `Regex should not match empty string`,\n\t\t\theaders:    http.Header{\"namespace\": []string{\".*\"}},\n\t\t\theaderName: \"namespace\",\n\t\t\tregexMatch: true,\n\t\t\tpromQuery:  `up{instance=\"localhost:9090\"} + foo{namespace=\"tenant1-.*\"}`,\n\t\t\texpCode:    http.StatusBadRequest,\n\t\t},\n\t} {\n\t\tfor _, endpoint := range []string{\"query\", \"query_range\", \"query_exemplars\"} {\n\t\t\tt.Run(endpoint+\"/\"+strings.ReplaceAll(tc.name, \" \", \"_\"), func(t *testing.T) {\n\t\t\t\tvar expBody string\n\t\t\t\tif tc.expPromQueryBody != \"\" {\n\t\t\t\t\texpBody = url.Values(map[string][]string{\"query\": {tc.expPromQueryBody}}).Encode()\n\t\t\t\t}\n\n\t\t\t\tmockHandler := checkQueryHandler(expBody, queryParam, tc.expPromQuery)\n\t\t\t\tif (len(tc.staticLabelVal) == 0) != (tc.headers == nil) {\n\t\t\t\t\tmockHandler = checkParameterAbsent(proxyLabel, mockHandler)\n\t\t\t\t}\n\t\t\t\tm := newMockUpstream(mockHandler)\n\t\t\t\tdefer m.Close()\n\t\t\t\tvar opts []Option\n\n\t\t\t\tif tc.errorOnReplace {\n\t\t\t\t\topts = append(opts, WithErrorOnReplace())\n\t\t\t\t}\n\t\t\t\tif tc.regexMatch {\n\t\t\t\t\topts = append(opts, WithRegexMatch())\n\t\t\t\t}\n\n\t\t\t\tvar labelEnforcer ExtractLabeler\n\t\t\t\tif len(tc.staticLabelVal) > 0 {\n\t\t\t\t\tlabelEnforcer = StaticLabelEnforcer(tc.staticLabelVal)\n\t\t\t\t} else if tc.headerName != \"\" {\n\t\t\t\t\tlabelEnforcer = HTTPHeaderEnforcer{Name: tc.headerName, ParseListSyntax: tc.headerUsesListSyntax}\n\t\t\t\t} else if tc.queryParam != \"\" {\n\t\t\t\t\tlabelEnforcer = HTTPFormEnforcer{ParameterName: tc.queryParam}\n\t\t\t\t} else {\n\t\t\t\t\tlabelEnforcer = HTTPFormEnforcer{ParameterName: proxyLabel}\n\t\t\t\t}\n\n\t\t\t\tr, err := NewRoutes(m.url, proxyLabel, labelEnforcer, opts...)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tu, err := url.Parse(\"http://prometheus.example.com/api/v1/\" + endpoint)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t\tq := u.Query()\n\t\t\t\tq.Set(queryParam, tc.promQuery)\n\t\t\t\tif tc.queryParam != \"\" {\n\t\t\t\t\tfor _, lv := range tc.labelv {\n\t\t\t\t\t\tq.Add(tc.queryParam, lv)\n\t\t\t\t\t}\n\t\t\t\t} else if len(tc.staticLabelVal) == 0 && tc.headerName == \"\" && len(tc.labelv) > 0 {\n\t\t\t\t\tfor _, lv := range tc.labelv {\n\t\t\t\t\t\tq.Add(proxyLabel, lv)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tu.RawQuery = q.Encode()\n\n\t\t\t\tvar b io.Reader = nil\n\t\t\t\tif tc.promQueryBody != \"\" {\n\t\t\t\t\tb = strings.NewReader(url.Values(map[string][]string{\"query\": {tc.promQueryBody}}).Encode())\n\t\t\t\t}\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(tc.method, u.String(), b)\n\t\t\t\tif tc.headers != nil {\n\t\t\t\t\treq.Header = tc.headers\n\t\t\t\t}\n\t\t\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\t\t\tr.ServeHTTP(w, req)\n\n\t\t\t\tresp := w.Result()\n\t\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t\tdefer resp.Body.Close()\n\n\t\t\t\tif resp.StatusCode != tc.expCode {\n\t\t\t\t\tt.Logf(\"expected status code %d, got %d\", tc.expCode, resp.StatusCode)\n\t\t\t\t\tt.Logf(\"%s\", string(body))\n\t\t\t\t\tt.FailNow()\n\t\t\t\t}\n\t\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif string(body) != string(tc.expResponse) {\n\t\t\t\t\tt.Fatalf(\"expected response body %q, got %q\", string(tc.expResponse), string(body))\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "injectproxy/rules.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage injectproxy\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/prometheus/prometheus/model/labels\"\n)\n\ntype apiResponse struct {\n\tStatus    string          `json:\"status\"`\n\tData      json.RawMessage `json:\"data,omitempty\"`\n\tErrorType string          `json:\"errorType,omitempty\"`\n\tError     string          `json:\"error,omitempty\"`\n\tWarnings  []string        `json:\"warnings,omitempty\"`\n}\n\nfunc getAPIResponse(resp *http.Response) (*apiResponse, error) {\n\tdefer resp.Body.Close()\n\treader := resp.Body\n\n\tif resp.Header.Get(\"Content-Encoding\") == \"gzip\" && !resp.Uncompressed {\n\t\tvar err error\n\t\treader, err = gzip.NewReader(resp.Body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"gzip decoding error: %w\", err)\n\t\t}\n\t\tdefer reader.Close()\n\n\t\t// TODO: recompress the modified response?\n\t\tresp.Header.Del(\"Content-Encoding\")\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t}\n\n\tvar apir apiResponse\n\tif err := json.NewDecoder(reader).Decode(&apir); err != nil {\n\t\treturn nil, fmt.Errorf(\"JSON decoding error: %w\", err)\n\t}\n\n\tif apir.Status != \"success\" {\n\t\treturn nil, fmt.Errorf(\"unexpected response status: %q\", apir.Status)\n\t}\n\n\treturn &apir, nil\n}\n\ntype rulesData struct {\n\tRuleGroups []*ruleGroup `json:\"groups\"`\n}\n\ntype ruleGroup struct {\n\tName     string  `json:\"name\"`\n\tFile     string  `json:\"file\"`\n\tRules    []rule  `json:\"rules\"`\n\tInterval float64 `json:\"interval\"`\n}\n\ntype rule struct {\n\t*alertingRule\n\t*recordingRule\n}\n\nfunc (r *rule) Labels() labels.Labels {\n\tif r.alertingRule != nil {\n\t\treturn r.alertingRule.Labels\n\t}\n\treturn r.recordingRule.Labels\n}\n\n// MarshalJSON implements the json.Marshaler interface for rule.\nfunc (r *rule) MarshalJSON() ([]byte, error) {\n\tif r.alertingRule != nil {\n\t\treturn json.Marshal(r.alertingRule)\n\t}\n\treturn json.Marshal(r.recordingRule)\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface for rule.\nfunc (r *rule) UnmarshalJSON(b []byte) error {\n\tvar ruleType struct {\n\t\tType string `json:\"type\"`\n\t}\n\tif err := json.Unmarshal(b, &ruleType); err != nil {\n\t\treturn err\n\t}\n\tswitch ruleType.Type {\n\tcase \"alerting\":\n\t\tvar alertingr alertingRule\n\t\tif err := json.Unmarshal(b, &alertingr); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tr.alertingRule = &alertingr\n\tcase \"recording\":\n\t\tvar recordingr recordingRule\n\t\tif err := json.Unmarshal(b, &recordingr); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tr.recordingRule = &recordingr\n\tdefault:\n\t\treturn fmt.Errorf(\"failed to unmarshal rule: unknown type %q\", ruleType.Type)\n\t}\n\n\treturn nil\n}\n\ntype alertingRule struct {\n\tState          string        `json:\"state\"`\n\tName           string        `json:\"name\"`\n\tQuery          string        `json:\"query\"`\n\tDuration       float64       `json:\"duration\"`\n\tKeepFiringFor  float64       `json:\"keepFiringFor\"`\n\tLabels         labels.Labels `json:\"labels\"`\n\tAnnotations    labels.Labels `json:\"annotations\"`\n\tAlerts         []*alert      `json:\"alerts\"`\n\tHealth         string        `json:\"health\"`\n\tLastError      string        `json:\"lastError,omitempty\"`\n\tEvaluationTime float64       `json:\"evaluationTime\"`\n\tLastEvaluation time.Time     `json:\"lastEvaluation\"`\n\t// Type of an alertingRule is always \"alerting\".\n\tType string `json:\"type\"`\n}\n\ntype recordingRule struct {\n\tName           string        `json:\"name\"`\n\tQuery          string        `json:\"query\"`\n\tLabels         labels.Labels `json:\"labels,omitempty\"`\n\tHealth         string        `json:\"health\"`\n\tLastError      string        `json:\"lastError,omitempty\"`\n\tEvaluationTime float64       `json:\"evaluationTime\"`\n\tLastEvaluation time.Time     `json:\"lastEvaluation\"`\n\t// Type of a recordingRule is always \"recording\".\n\tType string `json:\"type\"`\n}\n\ntype alertsData struct {\n\tAlerts []*alert `json:\"alerts\"`\n}\n\ntype alert struct {\n\tLabels          labels.Labels `json:\"labels\"`\n\tAnnotations     labels.Labels `json:\"annotations\"`\n\tState           string        `json:\"state\"`\n\tActiveAt        *time.Time    `json:\"activeAt,omitempty\"`\n\tKeepFiringSince *time.Time    `json:\"keepFiringSince,omitempty\"`\n\tValue           string        `json:\"value\"`\n}\n\n// errModifyResponseFailed is returned when the proxy failed to modify the\n// response from the backend.\nvar errModifyResponseFailed = errors.New(\"failed to process the API response\")\n\n// modifyAPIResponse unwraps the Prometheus API response, passes the enforced\n// label value and the response to the given function and finally replaces the\n// result in the response.\nfunc modifyAPIResponse(f func([]string, *http.Request, *apiResponse) (any, error)) func(*http.Response) error {\n\treturn func(resp *http.Response) error {\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t// Pass non-200 responses as-is.\n\t\t\treturn nil\n\t\t}\n\n\t\tapir, err := getAPIResponse(resp)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"can't decode the response: %w\", err)\n\t\t}\n\n\t\tv, err := f(MustLabelValues(resp.Request.Context()), resp.Request, apir)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"%w: %w\", errModifyResponseFailed, err)\n\t\t}\n\n\t\tb, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"can't encode the data: %w\", err)\n\t\t}\n\n\t\tapir.Data = json.RawMessage(b)\n\n\t\tvar buf bytes.Buffer\n\t\tif err = json.NewEncoder(&buf).Encode(apir); err != nil {\n\t\t\treturn fmt.Errorf(\"can't encode the response: %w\", err)\n\t\t}\n\t\tresp.Body = io.NopCloser(&buf)\n\t\tresp.Header[\"Content-Length\"] = []string{fmt.Sprint(buf.Len())}\n\n\t\treturn nil\n\t}\n}\n\nfunc (r *routes) filterRules(lvalues []string, req *http.Request, resp *apiResponse) (any, error) {\n\tvar rgs rulesData\n\tif err := json.Unmarshal(resp.Data, &rgs); err != nil {\n\t\treturn nil, fmt.Errorf(\"can't decode rules data: %w\", err)\n\t}\n\n\tm, err := r.newLabelMatcher(lvalues...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfiltered := []*ruleGroup{}\n\tfor _, rg := range rgs.RuleGroups {\n\t\tvar rules []rule\n\t\tfor _, rgr := range rg.Rules {\n\t\t\tif lval := rgr.Labels().Get(r.label); lval != \"\" && m.Matches(lval) {\n\t\t\t\trules = append(rules, rgr)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif !r.rulesWithActiveAlerts || rgr.alertingRule == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar ar *alertingRule\n\t\t\tfor i := range rgr.Alerts {\n\t\t\t\tif lval := rgr.Alerts[i].Labels.Get(r.label); lval == \"\" || !m.Matches(lval) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif ar == nil {\n\t\t\t\t\tar = &alertingRule{\n\t\t\t\t\t\tName:           rgr.alertingRule.Name,\n\t\t\t\t\t\tQuery:          rgr.alertingRule.Query,\n\t\t\t\t\t\tDuration:       rgr.Duration,\n\t\t\t\t\t\tKeepFiringFor:  rgr.KeepFiringFor,\n\t\t\t\t\t\tLabels:         rgr.alertingRule.Labels.Copy(),\n\t\t\t\t\t\tAnnotations:    rgr.Annotations.Copy(),\n\t\t\t\t\t\tHealth:         rgr.alertingRule.Health,\n\t\t\t\t\t\tLastError:      rgr.alertingRule.LastError,\n\t\t\t\t\t\tEvaluationTime: rgr.alertingRule.EvaluationTime,\n\t\t\t\t\t\tLastEvaluation: rgr.alertingRule.LastEvaluation,\n\t\t\t\t\t\tType:           rgr.alertingRule.Type,\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tar.Alerts = append(ar.Alerts, rgr.Alerts[i])\n\t\t\t\tswitch ar.State {\n\t\t\t\tcase \"pending\":\n\t\t\t\t\tif rgr.alertingRule.Alerts[i].State == \"firing\" {\n\t\t\t\t\t\tar.State = rgr.alertingRule.Alerts[i].State\n\t\t\t\t\t}\n\t\t\t\tcase \"\":\n\t\t\t\t\tar.State = rgr.alertingRule.Alerts[i].State\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ar != nil {\n\t\t\t\trules = append(rules, rule{alertingRule: ar})\n\t\t\t}\n\t\t}\n\n\t\tif len(rules) > 0 {\n\t\t\trg.Rules = rules\n\t\t\tfiltered = append(filtered, rg)\n\t\t}\n\t}\n\n\treturn &rulesData{RuleGroups: filtered}, nil\n}\n\nfunc (r *routes) filterAlerts(lvalues []string, _ *http.Request, resp *apiResponse) (any, error) {\n\tvar data alertsData\n\tif err := json.Unmarshal(resp.Data, &data); err != nil {\n\t\treturn nil, fmt.Errorf(\"can't decode alerts data: %w\", err)\n\t}\n\n\tm, err := r.newLabelMatcher(lvalues...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfiltered := []*alert{}\n\tfor _, alert := range data.Alerts {\n\t\tif lval := alert.Labels.Get(r.label); lval != \"\" && m.Matches(lval) {\n\t\t\tfiltered = append(filtered, alert)\n\t\t}\n\t}\n\n\treturn &alertsData{Alerts: filtered}, nil\n}\n"
  },
  {
    "path": "injectproxy/rules_test.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage injectproxy\n\nimport (\n\t\"compress/gzip\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/golden\"\n)\n\ntype gzipResponseWriter struct {\n\tio.Writer\n\thttp.ResponseWriter\n}\n\nfunc (w *gzipResponseWriter) Write(b []byte) (int, error) {\n\treturn w.Writer.Write(b)\n}\n\nfunc gzipHandler(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tgz := gzip.NewWriter(w)\n\t\tdefer gz.Close()\n\n\t\tw.Header().Del(\"Content-Length\")\n\t\tw.Header().Set(\"Content-Encoding\", \"gzip\")\n\t\tnext.ServeHTTP(&gzipResponseWriter{Writer: gz, ResponseWriter: w}, r)\n\t})\n}\n\nfunc validRulesWithLabelMatchers(exp ...string) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\tif err := req.ParseForm(); err != nil {\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\tw.Write([]byte(err.Error()))\n\t\t\treturn\n\t\t}\n\n\t\tmatchers := req.Form[\"match[]\"]\n\t\tif !reflect.DeepEqual(matchers, exp) {\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\tw.Write(fmt.Appendf(nil, \"invalid matchers:\\n- expected: %q\\n- got: %q\", exp, matchers))\n\t\t\treturn\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.Write([]byte(`{\n  \"status\": \"success\",\n  \"data\": {\n    \"groups\": [\n      {\n        \"name\": \"group1\",\n        \"file\": \"testdata/rules1.yml\",\n        \"rules\": [\n          {\n            \"name\": \"metric1\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns1\"\n            },\n            \"health\": \"ok\",\n            \"type\": \"recording\",\n            \"evaluationTime\": 0.000214303,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.403557247+02:00\"\n          }\n        ],\n        \"interval\": 10\n      }\n    ]\n  }\n}`))\n\t})\n}\n\nfunc validRules() http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.Write([]byte(`{\n  \"status\": \"success\",\n  \"data\": {\n    \"groups\": [\n      {\n        \"name\": \"group1\",\n        \"file\": \"testdata/rules1.yml\",\n        \"rules\": [\n          {\n            \"name\": \"metric1\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns1\"\n            },\n            \"health\": \"ok\",\n            \"type\": \"recording\",\n            \"evaluationTime\": 0.000214303,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.403557247+02:00\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"1\",\n            \"labels\": {\n              \"namespace\": \"ns1\",\n              \"operation\": \"create\"\n            },\n            \"health\": \"ok\",\n            \"type\": \"recording\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:53.403557247+02:00\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns1\",\n              \"operation\": \"update\"\n            },\n            \"health\": \"ok\",\n            \"type\": \"recording\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:54.403557247+02:00\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns1\",\n              \"operation\": \"delete\"\n            },\n            \"health\": \"ok\",\n            \"type\": \"recording\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:53.603557247+02:00\"\n          },\n          {\n            \"state\": \"firing\",\n            \"name\": \"Alert1\",\n            \"query\": \"metric1{namespace=\\\"ns1\\\"} == 0\",\n            \"duration\": 0,\n            \"labels\": {\n              \"namespace\": \"ns1\"\n            },\n            \"annotations\": {},\n            \"alerts\": [\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert1\",\n                  \"namespace\": \"ns1\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n                \"value\": \"0e+00\"\n              }\n            ],\n            \"health\": \"ok\",\n            \"type\": \"alerting\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:53.803557247+02:00\"\n          },\n          {\n            \"state\": \"firing\",\n            \"name\": \"Alert2\",\n            \"query\": \"metric2{namespace=\\\"ns1\\\"} == 0\",\n            \"duration\": 0,\n            \"labels\": {\n              \"namespace\": \"ns1\"\n            },\n            \"annotations\": {},\n            \"alerts\": [\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert2\",\n                  \"namespace\": \"ns1\",\n                  \"operation\": \"update\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n                \"value\": \"0e+00\"\n              },\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert2\",\n                  \"namespace\": \"ns1\",\n                  \"operation\": \"delete\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n                \"value\": \"0e+00\"\n              }\n            ],\n            \"health\": \"ok\",\n            \"type\": \"alerting\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:53.903557247+02:00\"\n          }\n        ],\n        \"interval\": 10\n      },\n      {\n        \"name\": \"group1\",\n        \"file\": \"testdata/rules2.yml\",\n        \"rules\": [\n          {\n            \"name\": \"metric1\",\n            \"query\": \"1\",\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"health\": \"ok\",\n            \"type\": \"recording\",\n            \"evaluationTime\": 0.000214303,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.403557247+02:00\"\n          },\n          {\n            \"state\": \"inactive\",\n            \"name\": \"Alert1\",\n            \"query\": \"metric1{namespace=\\\"ns2\\\"} == 0\",\n            \"duration\": 0,\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"annotations\": {},\n            \"alerts\": [],\n            \"health\": \"ok\",\n            \"type\": \"alerting\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.503557247+02:00\"\n          }\n        ],\n        \"interval\": 10\n      },\n      {\n        \"name\": \"group2\",\n        \"file\": \"testdata/rules2.yml\",\n        \"rules\": [\n          {\n            \"name\": \"metric2\",\n            \"query\": \"1\",\n            \"labels\": {\n              \"namespace\": \"ns2\",\n              \"operation\": \"create\"\n            },\n            \"health\": \"ok\",\n            \"type\": \"recording\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.503557247+02:00\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"2\",\n            \"labels\": {\n              \"namespace\": \"ns2\",\n              \"operation\": \"update\"\n            },\n            \"health\": \"ok\",\n            \"type\": \"recording\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.603557247+02:00\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"3\",\n            \"labels\": {\n              \"namespace\": \"ns2\",\n              \"operation\": \"delete\"\n            },\n            \"health\": \"ok\",\n            \"type\": \"recording\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.643557247+02:00\"\n          },\n          {\n            \"name\": \"metric3\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"health\": \"ok\",\n            \"type\": \"recording\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.683557247+02:00\"\n          },\n          {\n            \"state\": \"inactive\",\n            \"name\": \"Alert2\",\n            \"query\": \"metric2{namespace=\\\"ns2\\\"} == 0\",\n            \"duration\": 0,\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"annotations\": {},\n            \"alerts\": [],\n            \"health\": \"ok\",\n            \"type\": \"alerting\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.803557247+02:00\"\n          },\n          {\n            \"state\": \"firing\",\n            \"name\": \"Alert3\",\n            \"query\": \"metric3{namespace=\\\"ns2\\\"} == 0\",\n            \"duration\": 0,\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"annotations\": {},\n            \"alerts\": [\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert3\",\n                  \"namespace\": \"ns2\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:39.972915521+01:00\",\n                \"value\": \"0e+00\"\n              }\n            ],\n            \"health\": \"ok\",\n            \"type\": \"alerting\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.903557247+02:00\"\n          }\n        ],\n        \"interval\": 10\n      },\n      {\n        \"name\": \"group3\",\n        \"file\": \"testdata/rules3.yml\",\n        \"rules\": [\n          {\n            \"state\": \"firing\",\n            \"name\": \"Alert3\",\n            \"query\": \"metric4{ns!=\\\"default\\\"} == 0\",\n            \"duration\": 300,\n            \"labels\": {},\n            \"annotations\": {},\n            \"alerts\": [\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert3\",\n                  \"namespace\": \"ns1\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:39.972915521+01:00\",\n                \"value\": \"0e+00\"\n              },\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert3\",\n                  \"namespace\": \"ns3\"\n                },\n                \"annotations\": {},\n                \"state\": \"pending\",\n                \"activeAt\": \"2019-12-18T13:20:39.972915521+01:00\",\n                \"value\": \"0e+00\"\n              }\n            ],\n            \"health\": \"ok\",\n            \"type\": \"alerting\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.903557247+02:00\"\n          },\n          {\n            \"state\": \"firing\",\n            \"name\": \"Alert4\",\n            \"query\": \"metric5 == 0\",\n            \"duration\": 300,\n            \"labels\": {},\n            \"annotations\": {},\n            \"alerts\": [\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert4\",\n                  \"namespace\": \"ns3\",\n                  \"state\": \"foo\"\n                },\n                \"annotations\": {},\n                \"state\": \"pending\",\n                \"activeAt\": \"2019-12-18T13:20:39.972915521+01:00\",\n                \"value\": \"0e+00\"\n              },\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert1\",\n                  \"namespace\": \"ns3\",\n                  \"state\": \"bar\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:39.972915521+01:00\",\n                \"value\": \"0e+00\"\n              }\n            ],\n            \"health\": \"ok\",\n            \"type\": \"alerting\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.903557247+02:00\"\n          }\n        ],\n        \"interval\": 10\n      }\n    ]\n  }\n}`))\n\t})\n}\n\nfunc validAlerts() http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.Write([]byte(`{\n  \"status\": \"success\",\n  \"data\": {\n    \"alerts\": [\n      {\n        \"labels\": {\n          \"alertname\": \"Alert1\",\n          \"namespace\": \"ns1\"\n        },\n        \"annotations\": {},\n        \"state\": \"firing\",\n        \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n        \"value\": \"0e+00\"\n      },\n      {\n        \"labels\": {\n          \"alertname\": \"Alert2\",\n          \"namespace\": \"ns1\",\n          \"operation\": \"update\"\n        },\n        \"annotations\": {},\n        \"state\": \"firing\",\n        \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n        \"value\": \"0e+00\"\n      },\n      {\n        \"labels\": {\n          \"alertname\": \"Alert2\",\n          \"namespace\": \"ns1\",\n          \"operation\": \"delete\"\n        },\n        \"annotations\": {},\n        \"state\": \"firing\",\n        \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n        \"value\": \"0e+00\"\n      },\n      {\n        \"labels\": {\n          \"alertname\": \"Alert3\",\n          \"namespace\": \"ns2\"\n        },\n        \"annotations\": {},\n        \"state\": \"firing\",\n        \"activeAt\": \"2019-12-18T13:14:39.972915521+01:00\",\n        \"value\": \"0e+00\"\n      }\n    ]\n  }\n}`))\n\t})\n}\n\nfunc TestRules(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tlabelv     []string\n\t\tupstream   http.Handler\n\t\treqHeaders http.Header\n\t\topts       []Option\n\n\t\texpCode int\n\t\tgolden  string\n\t}{\n\t\t{\n\t\t\t// No \"namespace\" parameter returns an error.\n\t\t\texpCode: http.StatusBadRequest,\n\t\t\tgolden:  \"rules_no_namespace_error.golden\",\n\t\t},\n\t\t{\n\t\t\t// non 200 status code from upstream is passed as-is.\n\t\t\tlabelv: []string{\"upstream_error\"},\n\t\t\tupstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\tw.Write([]byte(\"error\"))\n\t\t\t}),\n\n\t\t\texpCode: http.StatusBadRequest,\n\t\t\tgolden:  \"rules_upstream_error.golden\",\n\t\t},\n\t\t{\n\t\t\t// incomplete API response triggers a 502 error.\n\t\t\tlabelv: []string{\"incomplete_data_from_upstream\"},\n\t\t\tupstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\t\tw.Write([]byte(\"{\"))\n\t\t\t}),\n\n\t\t\texpCode: http.StatusBadGateway,\n\t\t\tgolden:  \"rules_incomplete_upstream_response.golden\",\n\t\t},\n\t\t{\n\t\t\t// invalid API response triggers a 502 error.\n\t\t\tlabelv: []string{\"invalid_data_from_upstream\"},\n\t\t\tupstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\t\tw.Write([]byte(\"0\"))\n\t\t\t}),\n\n\t\t\texpCode: http.StatusBadGateway,\n\t\t\tgolden:  \"rules_invalid_upstream_response.golden\",\n\t\t},\n\t\t{\n\t\t\t// \"namespace\" parameter matching no rule.\n\t\t\tlabelv:   []string{\"not_present\"},\n\t\t\tupstream: validRules(),\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"rules_no_match.golden\",\n\t\t},\n\t\t{\n\t\t\t// Gzipped response should be handled when explictly asked by the original client.\n\t\t\tlabelv:   []string{\"not_present_gzip_requested\"},\n\t\t\tupstream: gzipHandler(validRules()),\n\t\t\treqHeaders: map[string][]string{\n\t\t\t\t\"Accept-Encoding\": {\"gzip\"},\n\t\t\t},\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"rules_no_match_with_gzip_requested.golden\",\n\t\t},\n\t\t{\n\t\t\t// When the client doesn't ask explicitly for gzip encoding, the Go\n\t\t\t// standard library will automatically ask for it and it will\n\t\t\t// transparently decompress the gzipped response.\n\t\t\tlabelv:   []string{\"not_present_gzip_not_requested\"},\n\t\t\tupstream: gzipHandler(validRules()),\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"rules_no_match_with_gzip_not_requested.golden\",\n\t\t},\n\t\t{\n\t\t\tlabelv:   []string{\"ns1\"},\n\t\t\tupstream: validRules(),\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"rules_match_namespace_ns1.golden\",\n\t\t},\n\t\t{\n\t\t\tlabelv:   []string{\"ns2\"},\n\t\t\tupstream: validRules(),\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"rules_match_namespace_ns2.golden\",\n\t\t},\n\t\t{\n\t\t\tlabelv:   []string{\"ns1\", \"ns2\"},\n\t\t\tupstream: validRules(),\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"rules_match_namespaces_ns1_and_ns2.golden\",\n\t\t},\n\t\t{\n\t\t\tlabelv:   []string{\"ns1|ns2\"},\n\t\t\tupstream: validRules(),\n\t\t\topts:     []Option{WithRegexMatch()},\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"rules_match_namespaces_ns1_and_ns2.golden\",\n\t\t},\n\t\t{\n\t\t\tlabelv:   []string{\"ns1|ns2\", \"ns3\"},\n\t\t\tupstream: validRules(),\n\t\t\topts:     []Option{WithRegexMatch()},\n\n\t\t\texpCode: http.StatusBadRequest,\n\t\t\tgolden:  \"rules_invalid_upstream_response.golden\",\n\t\t},\n\t\t{\n\t\t\tlabelv:   []string{\"ns3\"},\n\t\t\tupstream: validRules(),\n\t\t\topts:     []Option{WithActiveAlerts()},\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"rules_with_active_alerts.golden\",\n\t\t},\n\t\t{\n\t\t\tlabelv:   []string{\"ns1\"},\n\t\t\tupstream: validRulesWithLabelMatchers(\"{namespace=\\\"ns1\\\"}\"),\n\t\t\topts:     []Option{WithLabelMatchersForRulesAPI()},\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"rules_with_label_matchers.golden\",\n\t\t},\n\t} {\n\t\tt.Run(fmt.Sprintf(\"%s=%s\", proxyLabel, tc.labelv), func(t *testing.T) {\n\t\t\tm := newMockUpstream(tc.upstream)\n\t\t\tdefer m.Close()\n\t\t\tr, err := NewRoutes(\n\t\t\t\tm.url,\n\t\t\t\tproxyLabel,\n\t\t\t\tHTTPFormEnforcer{ParameterName: proxyLabel},\n\t\t\t\ttc.opts...,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tu, err := url.Parse(\"http://prometheus.example.com/api/v1/rules\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tq := u.Query()\n\t\t\tfor _, lv := range tc.labelv {\n\t\t\t\tq.Add(proxyLabel, lv)\n\t\t\t}\n\n\t\t\tu.RawQuery = q.Encode()\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", u.String(), nil)\n\t\t\tfor k, v := range tc.reqHeaders {\n\t\t\t\tfor i := range v {\n\t\t\t\t\treq.Header.Add(k, v[i])\n\t\t\t\t}\n\t\t\t}\n\t\t\tr.ServeHTTP(w, req)\n\n\t\t\tresp := w.Result()\n\n\t\t\tif resp.StatusCode != tc.expCode {\n\t\t\t\tb, err := io.ReadAll(resp.Body)\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Logf(\"body: %s\", b)\n\t\t\t\t}\n\t\t\t\tt.Fatalf(\"expected status code %d, got %d\", tc.expCode, resp.StatusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"expected no error, got %s\", err)\n\t\t\t}\n\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\tgolden.Assert(t, string(body), tc.golden)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// We need to unmarshal/marshal the result to run deterministic comparisons.\n\t\t\tgot := normalizeAPIResponse(t, body)\n\t\t\tgolden.Assert(t, got, tc.golden)\n\t\t})\n\t}\n}\n\nfunc TestAlerts(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tlabelv   []string\n\t\tupstream http.Handler\n\t\topts     []Option\n\n\t\texpCode int\n\t\tgolden  string\n\t}{\n\t\t{\n\t\t\t// No \"namespace\" parameter returns an error.\n\t\t\texpCode: http.StatusBadRequest,\n\t\t\tgolden:  \"alerts_no_namespace_error.golden\",\n\t\t},\n\t\t{\n\t\t\t// non 200 status code from upstream is passed as-is.\n\t\t\tlabelv: []string{\"upstream_error\"},\n\t\t\tupstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\tw.Write([]byte(\"error\"))\n\t\t\t}),\n\n\t\t\texpCode: http.StatusBadRequest,\n\t\t\tgolden:  \"alerts_upstream_error.golden\",\n\t\t},\n\t\t{\n\t\t\t// incomplete API response triggers a 502 error.\n\t\t\tlabelv: []string{\"incomplete_data_from_upstream\"},\n\t\t\tupstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\t\tw.Write([]byte(\"{\"))\n\t\t\t}),\n\n\t\t\texpCode: http.StatusBadGateway,\n\t\t\tgolden:  \"alerts_incomplete_upstream_response.golden\",\n\t\t},\n\t\t{\n\t\t\t// invalid API response triggers a 502 error.\n\t\t\tlabelv: []string{\"invalid_data_from_upstream\"},\n\t\t\tupstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\t\tw.Write([]byte(\"0\"))\n\t\t\t}),\n\n\t\t\texpCode: http.StatusBadGateway,\n\t\t\tgolden:  \"alerts_invalid_upstream_response.golden\",\n\t\t},\n\t\t{\n\t\t\t// \"namespace\" parameter matching no rule.\n\t\t\tlabelv:   []string{\"not_present\"},\n\t\t\tupstream: validAlerts(),\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"alerts_no_match.golden\",\n\t\t},\n\t\t{\n\t\t\tlabelv:   []string{\"ns1\"},\n\t\t\tupstream: validAlerts(),\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"alerts_match_namespace_ns1.golden\",\n\t\t},\n\t\t{\n\t\t\tlabelv:   []string{\"ns2\"},\n\t\t\tupstream: validAlerts(),\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"alerts_match_namespace_ns2.golden\",\n\t\t},\n\t\t{\n\t\t\tlabelv:   []string{\"ns1\", \"ns2\"},\n\t\t\tupstream: validAlerts(),\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"alerts_match_namespaces_ns1_and_ns2.golden\",\n\t\t},\n\t\t{\n\t\t\tlabelv:   []string{\"ns1|ns2\"},\n\t\t\tupstream: validAlerts(),\n\t\t\topts:     []Option{WithRegexMatch()},\n\n\t\t\texpCode: http.StatusOK,\n\t\t\tgolden:  \"alerts_match_namespaces_ns1_and_ns2.golden\",\n\t\t},\n\t\t{\n\t\t\tlabelv:   []string{\"ns1\", \"ns2\"},\n\t\t\tupstream: validAlerts(),\n\t\t\topts:     []Option{WithRegexMatch()},\n\n\t\t\texpCode: http.StatusBadRequest,\n\t\t\tgolden:  \"alerts_invalid_upstream_response.golden\",\n\t\t},\n\t} {\n\t\tt.Run(fmt.Sprintf(\"%s=%#v\", proxyLabel, tc.labelv), func(t *testing.T) {\n\t\t\tm := newMockUpstream(tc.upstream)\n\t\t\tdefer m.Close()\n\t\t\tr, err := NewRoutes(\n\t\t\t\tm.url,\n\t\t\t\tproxyLabel,\n\t\t\t\tHTTPFormEnforcer{ParameterName: proxyLabel},\n\t\t\t\ttc.opts...,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tu, err := url.Parse(\"http://prometheus.example.com/api/v1/alerts\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t\tq := u.Query()\n\t\t\tfor _, lv := range tc.labelv {\n\t\t\t\tq.Add(proxyLabel, lv)\n\t\t\t}\n\t\t\tu.RawQuery = q.Encode()\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", u.String(), nil)\n\t\t\tr.ServeHTTP(w, req)\n\n\t\t\tresp := w.Result()\n\n\t\t\tif resp.StatusCode != tc.expCode {\n\t\t\t\tt.Fatalf(\"expected status code %d, got %d\", tc.expCode, resp.StatusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"expected no error, got %s\", err)\n\t\t\t}\n\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\tgolden.Assert(t, string(body), tc.golden)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// We need to unmarshal/marshal the result to run deterministic comparisons.\n\t\t\tgot := normalizeAPIResponse(t, body)\n\t\t\tgolden.Assert(t, got, tc.golden)\n\t\t})\n\t}\n}\n\nfunc normalizeAPIResponse(t *testing.T, b []byte) string {\n\tt.Helper()\n\tvar apir apiResponse\n\tif err := json.Unmarshal(b, &apir); err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tout, err := json.MarshalIndent(&apir, \"\", \"  \")\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\n\treturn string(out)\n}\n"
  },
  {
    "path": "injectproxy/silences.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage injectproxy\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"path\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\truntimeclient \"github.com/go-openapi/runtime/client\"\n\t\"github.com/go-openapi/strfmt\"\n\t\"github.com/prometheus/alertmanager/api/v2/client\"\n\t\"github.com/prometheus/alertmanager/api/v2/client/silence\"\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n\t\"github.com/prometheus/alertmanager/pkg/labels\"\n)\n\n// silences proxies HTTP requests to the Alertmanager /api/v2/silences endpoint.\nfunc (r *routes) silences(w http.ResponseWriter, req *http.Request) {\n\tswitch req.Method {\n\tcase \"GET\":\n\t\tr.enforceFilterParameter(w, req)\n\tcase \"POST\":\n\t\tr.postSilence(w, req)\n\tdefault:\n\t\thttp.NotFound(w, req)\n\t}\n}\n\n// assertSingleLabelValue verifies that the proxy is configured to match only\n// one label value. If not, it will reply with \"422 Unprocessable Content\".\nfunc assertSingleLabelValue(next http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, req *http.Request) {\n\t\tlabelValues := MustLabelValues(req.Context())\n\t\tif len(labelValues) > 1 {\n\t\t\thttp.Error(w, \"Multiple label matchers not supported\", http.StatusUnprocessableEntity)\n\t\t\treturn\n\t\t}\n\n\t\tnext(w, req)\n\t}\n}\n\n// enforceFilterParameter injects a label matcher parameter into the\n// Alertmanager API's query.\nfunc (r *routes) enforceFilterParameter(w http.ResponseWriter, req *http.Request) {\n\tvar (\n\t\tq               = req.URL.Query()\n\t\tproxyLabelMatch labels.Matcher\n\t)\n\n\tif len(MustLabelValues(req.Context())) > 1 {\n\t\tproxyLabelMatch = labels.Matcher{\n\t\t\tType:  labels.MatchRegexp,\n\t\t\tName:  r.label,\n\t\t\tValue: labelValuesToRegexpString(MustLabelValues(req.Context())),\n\t\t}\n\t} else {\n\t\tmatcherType := labels.MatchEqual\n\t\tmatcherValue := MustLabelValue(req.Context())\n\t\tif r.regexMatch {\n\t\t\tcompiledRegex, err := regexp.Compile(matcherValue)\n\t\t\tif err != nil {\n\t\t\t\tprometheusAPIError(w, err.Error(), http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif compiledRegex.MatchString(\"\") {\n\t\t\t\tprometheusAPIError(w, \"Regex should not match empty string\", http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmatcherType = labels.MatchRegexp\n\t\t}\n\t\tproxyLabelMatch = labels.Matcher{\n\t\t\tType:  matcherType,\n\t\t\tName:  r.label,\n\t\t\tValue: matcherValue,\n\t\t}\n\t}\n\n\tmodified := []string{proxyLabelMatch.String()}\n\tfor _, filter := range q[\"filter\"] {\n\t\tm, err := labels.ParseMatcher(filter)\n\t\tif err != nil {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"bad request: can't parse filter %q: %v\", filter, err), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\t// Keep the original matcher in case of multi label values because\n\t\t// the user might want to filter on a specific value.\n\t\tif m.Name == r.label && proxyLabelMatch.Type != labels.MatchRegexp {\n\t\t\tcontinue\n\t\t}\n\n\t\tmodified = append(modified, filter)\n\t}\n\n\tq[\"filter\"] = modified\n\tq.Del(r.label)\n\treq.URL.RawQuery = q.Encode()\n\n\tr.handler.ServeHTTP(w, req)\n}\n\nfunc (r *routes) postSilence(w http.ResponseWriter, req *http.Request) {\n\tvar (\n\t\tsil    models.PostableSilence\n\t\tlvalue = MustLabelValue(req.Context())\n\t)\n\n\tif err := json.NewDecoder(req.Body).Decode(&sil); err != nil {\n\t\tprometheusAPIError(w, fmt.Sprintf(\"bad request: can't decode: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif sil.ID != \"\" {\n\t\t// This is an update for an existing silence.\n\t\texisting, err := r.getSilenceByID(req.Context(), sil.ID)\n\t\tif err != nil {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"proxy error: can't get silence: %v\", err), http.StatusBadGateway)\n\t\t\treturn\n\t\t}\n\n\t\tif !hasMatcherForLabel(existing.Matchers, r.label, lvalue) {\n\t\t\tprometheusAPIError(w, \"forbidden\", http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\t}\n\n\tvar falsy bool\n\tmodified := models.Matchers{\n\t\t&models.Matcher{Name: &(r.label), Value: &lvalue, IsRegex: &falsy},\n\t}\n\tfor _, m := range sil.Matchers {\n\t\tif m.Name != nil && *m.Name == r.label {\n\t\t\tcontinue\n\t\t}\n\t\tmodified = append(modified, m)\n\t}\n\t// At least one matcher in addition to the enforced label is required,\n\t// otherwise all alerts would be silenced\n\tif len(modified) < 2 {\n\t\tprometheusAPIError(w, \"need at least one matcher, got none\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tsil.Matchers = modified\n\n\tvar buf bytes.Buffer\n\tif err := json.NewEncoder(&buf).Encode(&sil); err != nil {\n\t\tprometheusAPIError(w, fmt.Sprintf(\"can't encode: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\treq = req.Clone(req.Context())\n\treq.Body = io.NopCloser(&buf)\n\treq.URL.RawQuery = \"\"\n\treq.Header[\"Content-Length\"] = []string{strconv.Itoa(buf.Len())}\n\treq.ContentLength = int64(buf.Len())\n\n\tr.handler.ServeHTTP(w, req)\n}\n\n// deleteSilence proxies HTTP requests to the Alertmanager /api/v2/silence/ endpoint.\nfunc (r *routes) deleteSilence(w http.ResponseWriter, req *http.Request) {\n\tsilID := strings.TrimPrefix(req.URL.Path, \"/api/v2/silence/\")\n\tif silID == \"\" || silID == req.URL.Path {\n\t\tprometheusAPIError(w, \"bad request\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t// Get the silence by ID and verify that it has the expected label.\n\tsil, err := r.getSilenceByID(req.Context(), silID)\n\tif err != nil {\n\t\tprometheusAPIError(w, fmt.Sprintf(\"proxy error: %v\", err), http.StatusBadGateway)\n\t\treturn\n\t}\n\n\tif !hasMatcherForLabel(sil.Matchers, r.label, MustLabelValue(req.Context())) {\n\t\tprometheusAPIError(w, \"forbidden\", http.StatusForbidden)\n\t\treturn\n\t}\n\n\treq.URL.RawQuery = \"\"\n\tr.handler.ServeHTTP(w, req)\n}\n\nfunc (r *routes) getSilenceByID(ctx context.Context, id string) (*models.GettableSilence, error) {\n\tamc := client.New(\n\t\truntimeclient.New(r.upstream.Host, path.Join(r.upstream.Path, \"/api/v2\"), []string{r.upstream.Scheme}),\n\t\tstrfmt.Default,\n\t)\n\tparams := silence.NewGetSilenceParams().WithContext(ctx)\n\tparams.SetSilenceID(strfmt.UUID(id))\n\tsil, err := amc.Silence.GetSilence(params)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn sil.Payload, nil\n}\n\nfunc hasMatcherForLabel(matchers models.Matchers, name, value string) bool {\n\tfor _, m := range matchers {\n\t\tif *m.Name == name && !*m.IsRegex && *m.Value == value {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "injectproxy/silences_test.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage injectproxy\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/prometheus/alertmanager/api/v2/models\"\n)\n\nfunc TestListSilences(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tlabelv     []string\n\t\tfilters    []string\n\t\tregexMatch bool\n\n\t\texpCode    int\n\t\texpFilters []string\n\t\texpBody    []byte\n\t}{\n\t\t{\n\t\t\t// No \"namespace\" parameter returns an error.\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\t// No \"filter\" parameter.\n\t\t\tlabelv:     []string{\"default\"},\n\t\t\texpCode:    http.StatusOK,\n\t\t\texpFilters: []string{`namespace=\"default\"`},\n\t\t\texpBody:    okResponse,\n\t\t},\n\t\t{\n\t\t\t// Many \"filter\" parameters.\n\t\t\tlabelv:     []string{\"default\"},\n\t\t\tfilters:    []string{`job=\"prometheus\"`, `instance=~\".+\"`},\n\t\t\texpCode:    http.StatusOK,\n\t\t\texpFilters: []string{`job=\"prometheus\"`, `instance=~\".+\"`, `namespace=\"default\"`},\n\t\t\texpBody:    okResponse,\n\t\t},\n\t\t{\n\t\t\t// Many \"filter\" parameters with a \"namespace\" label that needs to be enforced.\n\t\t\tlabelv:     []string{\"default\"},\n\t\t\tfilters:    []string{`namespace=~\"foo|default\"`, `job=\"prometheus\"`},\n\t\t\texpCode:    http.StatusOK,\n\t\t\texpFilters: []string{`namespace=\"default\"`, `job=\"prometheus\"`},\n\t\t\texpBody:    okResponse,\n\t\t},\n\t\t{\n\t\t\t// Invalid \"filter\" parameter.\n\t\t\tlabelv:  []string{\"default\"},\n\t\t\tfilters: []string{`namespace=~\"foo|default\"`, `job=\"promethe`},\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\t// Multiple label values are not supported.\n\t\t\tlabelv:  []string{\"default\", \"something\"},\n\t\t\texpCode: http.StatusUnprocessableEntity,\n\t\t},\n\t\t{\n\t\t\t// Regex match\n\t\t\tlabelv:     []string{\"tenant1-.*\"},\n\t\t\tregexMatch: true,\n\t\t\tfilters:    []string{`namespace=~\"foo|default\"`, `job=\"prometheus\"`},\n\t\t\texpCode:    http.StatusNotImplemented,\n\t\t},\n\t} {\n\t\tt.Run(strings.Join(tc.filters, \"&\"), func(t *testing.T) {\n\t\t\tm := newMockUpstream(checkQueryHandler(\"\", \"filter\", tc.expFilters...))\n\t\t\tdefer m.Close()\n\t\t\tvar opts []Option\n\t\t\tif tc.regexMatch {\n\t\t\t\topts = append(opts, WithRegexMatch())\n\t\t\t}\n\t\t\tr, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, opts...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tu, err := url.Parse(\"http://alertmanager.example.com/api/v2/silences\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t\tq := u.Query()\n\t\t\tfor _, m := range tc.filters {\n\t\t\t\tq.Add(\"filter\", m)\n\t\t\t}\n\t\t\tfor _, s := range tc.labelv {\n\t\t\t\tq.Add(proxyLabel, s)\n\t\t\t}\n\t\t\tu.RawQuery = q.Encode()\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", u.String(), nil)\n\t\t\tr.ServeHTTP(w, req)\n\n\t\t\tresp := w.Result()\n\t\t\tbody, _ := io.ReadAll(resp.Body)\n\t\t\tdefer resp.Body.Close()\n\n\t\t\tif resp.StatusCode != tc.expCode {\n\t\t\t\tt.Logf(\"expected status code %d, got %d\", tc.expCode, resp.StatusCode)\n\t\t\t\tt.Logf(\"%s\", string(body))\n\t\t\t\tt.FailNow()\n\t\t\t}\n\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif string(body) != string(tc.expBody) {\n\t\t\t\tt.Fatalf(\"expected body %q, got %q\", string(tc.expBody), string(body))\n\t\t\t}\n\t\t})\n\t}\n}\n\nconst silID = \"802146e0-1f7a-42a6-ab0e-1e631479970b\"\n\nfunc getSilenceWithoutLabel() http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\tif req.Method != \"GET\" {\n\t\t\tprometheusAPIError(w, \"invalid method: \"+req.Method, http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tif req.URL.Path != \"/api/v2/silence/\"+silID {\n\t\t\tprometheusAPIError(w, \"invalid path: \"+req.URL.Path, http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tfmt.Fprintf(w, `\n{\n  \"id\": \"%s\",\n  \"status\": {\n    \"state\": \"pending\"\n  },\n  \"updatedAt\": \"2020-01-15T09:06:23.419Z\",\n  \"comment\": \"comment\",\n  \"createdBy\": \"author\",\n  \"endsAt\": \"2020-02-13T13:00:02.084Z\",\n  \"matchers\": [\n    {\n      \"isRegex\": false,\n      \"name\": \"foo\",\n      \"value\": \"bar\"\n    }\n  ],\n  \"startsAt\": \"2020-02-13T12:02:01.000Z\"\n}\n\t\t\t\t`, silID)\n\t})\n}\n\nfunc getSilenceWithLabel(labelv string) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\tif req.Method != \"GET\" {\n\t\t\tprometheusAPIError(w, \"invalid method: \"+req.Method, http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tif req.URL.Path != \"/api/v2/silence/\"+silID {\n\t\t\tprometheusAPIError(w, \"invalid path: \"+req.URL.Path, http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tfmt.Fprintf(w, `\n{\n  \"id\": \"%s\",\n  \"status\": {\n    \"state\": \"pending\"\n  },\n  \"updatedAt\": \"2020-01-15T09:06:23.419Z\",\n  \"comment\": \"comment\",\n  \"createdBy\": \"author\",\n  \"endsAt\": \"2020-02-13T13:00:02.084Z\",\n  \"matchers\": [\n    {\n      \"isRegex\": false,\n      \"name\": \"%s\",\n      \"value\": \"%s\"\n    }\n  ],\n  \"startsAt\": \"2020-02-13T12:02:01.000Z\"\n}\n\t\t\t\t`, silID, proxyLabel, labelv)\n\t})\n}\n\nfunc createSilenceWithLabel(labelv string) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\tvar sil models.PostableSilence\n\t\tif err := json.NewDecoder(req.Body).Decode(&sil); err != nil {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"unexpected error: %v\", err), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tvar values []string\n\t\tfor _, m := range sil.Matchers {\n\t\t\tif *m.Name == proxyLabel {\n\t\t\t\tvalues = append(values, *m.Value)\n\t\t\t}\n\t\t}\n\t\tif len(values) != 1 {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"expected 1 matcher for label %s, got %d\", proxyLabel, len(values)), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tif values[0] != labelv {\n\t\t\tprometheusAPIError(w, fmt.Sprintf(\"expected matcher for label %s to be %q, got %q\", proxyLabel, labelv, values[0]), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write(okResponse)\n\t})\n}\n\n// chainedHandlers runs the handler one after the other.\ntype chainedHandlers struct {\n\tidx      int\n\thandlers []http.Handler\n}\n\nfunc (c *chainedHandlers) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tdefer func() { c.idx++ }()\n\n\tif c.idx >= len(c.handlers) {\n\t\tprometheusAPIError(w, \"\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tc.handlers[c.idx].ServeHTTP(w, req)\n}\n\nfunc TestDeleteSilence(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tID         string\n\t\tlabelv     []string\n\t\tupstream   http.Handler\n\t\tregexMatch bool\n\n\t\texpCode int\n\t\texpBody []byte\n\t}{\n\t\t{\n\t\t\t// No \"namespace\" parameter returns an error.\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\t// Missing silence ID.\n\t\t\tID:      \"\",\n\t\t\tlabelv:  []string{\"default\"},\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\t// The silence doesn't exist upstream.\n\t\t\tID:     silID,\n\t\t\tlabelv: []string{\"default\"},\n\t\t\tupstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\t\thttp.NotFound(w, req)\n\t\t\t}),\n\t\t\texpCode: http.StatusBadGateway,\n\t\t},\n\t\t{\n\t\t\t// The silence doesn't contain the expected label.\n\t\t\tID:       silID,\n\t\t\tlabelv:   []string{\"default\"},\n\t\t\tupstream: getSilenceWithoutLabel(),\n\t\t\texpCode:  http.StatusForbidden,\n\t\t},\n\t\t{\n\t\t\t// The silence doesn't have the expected value for the label.\n\t\t\tID:       silID,\n\t\t\tlabelv:   []string{\"default\"},\n\t\t\tupstream: getSilenceWithLabel(\"not default\"),\n\t\t\texpCode:  http.StatusForbidden,\n\t\t},\n\t\t{\n\t\t\t// The silence has the expected value for the label.\n\t\t\tID:     silID,\n\t\t\tlabelv: []string{\"default\"},\n\t\t\tupstream: &chainedHandlers{\n\t\t\t\thandlers: []http.Handler{\n\t\t\t\t\tgetSilenceWithLabel(\"default\"),\n\t\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\t\t\t\tw.Write([]byte(\"ok\"))\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpCode: http.StatusOK,\n\t\t\texpBody: []byte(\"ok\"),\n\t\t},\n\t\t{\n\t\t\t// The silence has the expected value for the label but upstream returns an error.\n\t\t\tID:     silID,\n\t\t\tlabelv: []string{\"default\"},\n\t\t\tupstream: &chainedHandlers{\n\t\t\t\thandlers: []http.Handler{\n\t\t\t\t\tgetSilenceWithLabel(\"default\"),\n\t\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\t\t\t\tw.WriteHeader(http.StatusTeapot)\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpCode: http.StatusTeapot,\n\t\t},\n\t\t{\n\t\t\t// Multiple label values are not supported.\n\t\t\tlabelv:  []string{\"default\", \"something\"},\n\t\t\texpCode: http.StatusUnprocessableEntity,\n\t\t},\n\t\t{\n\t\t\t// Regexp is not supported.\n\t\t\tlabelv:     []string{\"default\"},\n\t\t\tregexMatch: true,\n\t\t\texpCode:    http.StatusNotImplemented,\n\t\t},\n\t} {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tm := newMockUpstream(tc.upstream)\n\t\t\tdefer m.Close()\n\t\t\tvar opts []Option\n\t\t\tif tc.regexMatch {\n\t\t\t\topts = append(opts, WithRegexMatch())\n\t\t\t}\n\t\t\tr, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, opts...)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tu, err := url.Parse(fmt.Sprintf(\"http://alertmanager.example.com/api/v2/silence/%s\", tc.ID))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t\tq := u.Query()\n\t\t\tfor _, s := range tc.labelv {\n\t\t\t\tq.Add(proxyLabel, s)\n\t\t\t}\n\t\t\tu.RawQuery = q.Encode()\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"DELETE\", u.String(), nil)\n\t\t\tr.ServeHTTP(w, req)\n\n\t\t\tresp := w.Result()\n\t\t\tbody, _ := io.ReadAll(resp.Body)\n\t\t\tdefer resp.Body.Close()\n\n\t\t\tif resp.StatusCode != tc.expCode {\n\t\t\t\tt.Logf(\"expected status code %d, got %d\", tc.expCode, resp.StatusCode)\n\t\t\t\tt.Logf(\"%s\", string(body))\n\t\t\t\tt.FailNow()\n\t\t\t}\n\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif string(body) != string(tc.expBody) {\n\t\t\t\tt.Fatalf(\"expected body %q, got %q\", string(tc.expBody), string(body))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUpdateSilence(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tdata     string\n\t\tlabelv   []string\n\t\tupstream http.Handler\n\n\t\texpCode int\n\t\texpBody []byte\n\t}{\n\t\t{\n\t\t\t// No \"namespace\" parameter returns an error.\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\t// Invalid silence payload returns an error.\n\t\t\tdata:    \"{\",\n\t\t\tlabelv:  []string{\"default\"},\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\t// Creation of a valid silence without namespace label is ok.\n\t\t\tdata: `{\n    \"comment\":\"foo\",\n    \"createdBy\":\"bar\",\n    \"endsAt\":\"2020-02-13T13:00:02.084Z\",\n    \"matchers\": [\n        {\"isRegex\":false,\"Name\":\"foo\",\"Value\":\"bar\"}\n    ],\n    \"startsAt\":\"2020-02-13T12:02:01Z\"\n}`,\n\t\t\tlabelv:   []string{\"default\"},\n\t\t\tupstream: createSilenceWithLabel(\"default\"),\n\n\t\t\texpCode: http.StatusOK,\n\t\t\texpBody: okResponse,\n\t\t},\n\t\t{\n\t\t\t// Creation of a silence with an existing namespace label is ok.\n\t\t\tdata: `{\n    \"comment\":\"foo\",\n    \"createdBy\":\"bar\",\n    \"endsAt\":\"2020-02-13T13:00:02.084Z\",\n    \"matchers\": [\n        {\"isRegex\":false,\"Name\":\"foo\",\"Value\":\"bar\"},\n\t\t{\"isRegex\":false,\"Name\":\"namespace\",\"Value\":\"not default\"}\n    ],\n    \"startsAt\":\"2020-02-13T12:02:01Z\"\n}`,\n\t\t\tlabelv:   []string{\"default\"},\n\t\t\tupstream: createSilenceWithLabel(\"default\"),\n\n\t\t\texpCode: http.StatusOK,\n\t\t\texpBody: okResponse,\n\t\t},\n\t\t{\n\t\t\t// Creation of a silence without matcher returns an error.\n\t\t\tdata: `{\n    \"comment\":\"foo\",\n    \"createdBy\":\"bar\",\n    \"endsAt\":\"2020-02-13T13:00:02.084Z\",\n    \"matchers\": [],\n    \"startsAt\":\"2020-02-13T12:02:01Z\"\n}`,\n\t\t\tlabelv: []string{\"default\"},\n\n\t\t\texpCode: http.StatusBadRequest,\n\t\t},\n\t\t{\n\t\t\t// Update of an existing silence with a matching label is ok.\n\t\t\tdata: `{\n    \"id\":\"` + silID + `\",\n    \"comment\":\"foo\",\n    \"createdBy\":\"bar\",\n    \"endsAt\":\"2020-02-13T13:00:02.084Z\",\n    \"matchers\": [\n        {\"isRegex\":false,\"Name\":\"foo\",\"Value\":\"bar\"}\n    ],\n    \"startsAt\":\"2020-02-13T12:02:01Z\"\n}`,\n\t\t\tlabelv: []string{\"default\"},\n\t\t\tupstream: &chainedHandlers{\n\t\t\t\thandlers: []http.Handler{\n\t\t\t\t\tgetSilenceWithLabel(\"default\"),\n\t\t\t\t\tcreateSilenceWithLabel(\"default\"),\n\t\t\t\t},\n\t\t\t},\n\n\t\t\texpCode: http.StatusOK,\n\t\t\texpBody: okResponse,\n\t\t},\n\t\t{\n\t\t\t// Update of an existing silence with a non-matching label is denied.\n\t\t\tdata: `{\n    \"id\":\"` + silID + `\",\n    \"comment\":\"foo\",\n    \"createdBy\":\"bar\",\n    \"endsAt\":\"2020-02-13T13:00:02.084Z\",\n    \"matchers\": [\n        {\"isRegex\":false,\"Name\":\"foo\",\"Value\":\"bar\"}\n    ],\n    \"startsAt\":\"2020-02-13T12:02:01Z\"\n}`,\n\t\t\tlabelv: []string{\"default\"},\n\t\t\tupstream: &chainedHandlers{\n\t\t\t\thandlers: []http.Handler{\n\t\t\t\t\tgetSilenceWithLabel(\"not default\"),\n\t\t\t\t\tcreateSilenceWithLabel(\"default\"),\n\t\t\t\t},\n\t\t\t},\n\n\t\t\texpCode: http.StatusForbidden,\n\t\t},\n\t\t{\n\t\t\t// Update of a non-existing silence fails.\n\t\t\tdata: `{\n    \"id\":\"does not exist\",\n    \"comment\":\"foo\",\n    \"createdBy\":\"bar\",\n    \"endsAt\":\"2020-02-13T13:00:02.084Z\",\n    \"matchers\": [\n        {\"isRegex\":false,\"Name\":\"foo\",\"Value\":\"bar\"}\n    ],\n    \"startsAt\":\"2020-02-13T12:02:01Z\"\n}`,\n\t\t\tlabelv: []string{\"default\"},\n\t\t\tupstream: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\t\thttp.NotFound(w, req)\n\t\t\t}),\n\n\t\t\texpCode: http.StatusBadGateway,\n\t\t},\n\t\t{\n\t\t\t// The silence has the expected value for the label but upstream returns an error.\n\t\t\tdata: `{\n    \"id\":\"` + silID + `\",\n    \"comment\":\"foo\",\n    \"createdBy\":\"bar\",\n    \"endsAt\":\"2020-02-13T13:00:02.084Z\",\n    \"matchers\": [\n        {\"isRegex\":false,\"Name\":\"foo\",\"Value\":\"bar\"}\n    ],\n    \"startsAt\":\"2020-02-13T12:02:01Z\"\n}`,\n\t\t\tlabelv: []string{\"default\"},\n\t\t\tupstream: &chainedHandlers{\n\t\t\t\thandlers: []http.Handler{\n\t\t\t\t\tgetSilenceWithLabel(\"default\"),\n\t\t\t\t\thttp.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\t\t\t\tw.WriteHeader(http.StatusTeapot)\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpCode: http.StatusTeapot,\n\t\t},\n\t\t{\n\t\t\t// Multiple label values are not supported.\n\t\t\tlabelv:  []string{\"default\", \"something\"},\n\t\t\texpCode: http.StatusUnprocessableEntity,\n\t\t},\n\t} {\n\t\tt.Run(\"\", func(t *testing.T) {\n\t\t\tm := newMockUpstream(tc.upstream)\n\t\t\tdefer m.Close()\n\t\t\tr, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel})\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tu, err := url.Parse(\"http://alertmanager.example.com/api/v2/silences/\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t\tq := u.Query()\n\t\t\tfor _, s := range tc.labelv {\n\t\t\t\tq.Add(proxyLabel, s)\n\t\t\t}\n\t\t\tu.RawQuery = q.Encode()\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"POST\", u.String(), bytes.NewBufferString(tc.data))\n\t\t\tr.ServeHTTP(w, req)\n\n\t\t\tresp := w.Result()\n\t\t\tbody, _ := io.ReadAll(resp.Body)\n\t\t\tdefer resp.Body.Close()\n\n\t\t\tif resp.StatusCode != tc.expCode {\n\t\t\t\tt.Logf(\"expected status code %d, got %d\", tc.expCode, resp.StatusCode)\n\t\t\t\tt.Logf(\"%s\", string(body))\n\t\t\t\tt.FailNow()\n\t\t\t}\n\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif string(body) != string(tc.expBody) {\n\t\t\t\tt.Fatalf(\"expected body %q, got %q\", string(tc.expBody), string(body))\n\t\t\t}\n\t\t})\n\t}\n}\nfunc TestGetAlertGroups(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tlabelv         []string\n\t\tfilters        []string\n\t\texpCode        int\n\t\texpQueryValues []string\n\t\tqueryParam     string\n\t\turl            string\n\t}{\n\t\t{\n\t\t\t// No \"namespace\" parameter returns an error.\n\t\t\texpCode: http.StatusBadRequest,\n\t\t\turl:     \"http://alertmanager.example.com/api/v2/alerts/groups\",\n\t\t},\n\t\t{\n\t\t\t// Check for other query parameters\n\t\t\tlabelv:         []string{\"default\"},\n\t\t\texpCode:        http.StatusOK,\n\t\t\texpQueryValues: []string{\"false\"},\n\t\t\tqueryParam:     \"silenced\",\n\t\t\turl:            \"http://alertmanager.example.com/api/v2/alerts/groups?silenced=false\",\n\t\t},\n\t\t{\n\t\t\t// Check for filter parameter.\n\t\t\tlabelv:         []string{\"default\"},\n\t\t\tfilters:        []string{`job=\"prometheus\"`, `instance=~\".+\"`},\n\t\t\texpCode:        http.StatusOK,\n\t\t\texpQueryValues: []string{`job=\"prometheus\"`, `instance=~\".+\"`, `namespace=\"default\"`},\n\t\t\tqueryParam:     \"filter\",\n\t\t\turl:            \"http://alertmanager.example.com/api/v2/alerts/groups\",\n\t\t},\n\t\t{\n\t\t\t// Check for filter parameter with multiple label values.\n\t\t\tlabelv:         []string{\"default\", \"something\"},\n\t\t\tfilters:        []string{`job=\"prometheus\"`, `instance=~\".+\"`},\n\t\t\texpCode:        http.StatusOK,\n\t\t\texpQueryValues: []string{`job=\"prometheus\"`, `instance=~\".+\"`, `namespace=~\"default|something\"`},\n\t\t\tqueryParam:     \"filter\",\n\t\t\turl:            \"http://alertmanager.example.com/api/v2/alerts/groups\",\n\t\t},\n\t} {\n\t\tt.Run(strings.Join(tc.filters, \"&\"), func(t *testing.T) {\n\t\t\tm := newMockUpstream(checkQueryHandler(\"\", tc.queryParam, tc.expQueryValues...))\n\t\t\tdefer m.Close()\n\t\t\tr, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel})\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tu, err := url.Parse(tc.url)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t\tq := u.Query()\n\t\t\tfor _, m := range tc.filters {\n\t\t\t\tq.Add(\"filter\", m)\n\t\t\t}\n\t\t\tfor _, s := range tc.labelv {\n\t\t\t\tq.Add(proxyLabel, s)\n\t\t\t}\n\t\t\tu.RawQuery = q.Encode()\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", u.String(), nil)\n\t\t\tr.ServeHTTP(w, req)\n\n\t\t\tresp := w.Result()\n\t\t\tbody, _ := io.ReadAll(resp.Body)\n\t\t\tdefer resp.Body.Close()\n\n\t\t\tif resp.StatusCode != tc.expCode {\n\t\t\t\tt.Logf(\"expected status code %d, got %d\", tc.expCode, resp.StatusCode)\n\t\t\t\tt.Logf(\"%s\", string(body))\n\t\t\t\tt.FailNow()\n\t\t\t}\n\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "injectproxy/testdata/alerts_incomplete_upstream_response.golden",
    "content": ""
  },
  {
    "path": "injectproxy/testdata/alerts_invalid_upstream_response.golden",
    "content": ""
  },
  {
    "path": "injectproxy/testdata/alerts_match_namespace_ns1.golden",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {\n    \"alerts\": [\n      {\n        \"labels\": {\n          \"alertname\": \"Alert1\",\n          \"namespace\": \"ns1\"\n        },\n        \"annotations\": {},\n        \"state\": \"firing\",\n        \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n        \"value\": \"0e+00\"\n      },\n      {\n        \"labels\": {\n          \"alertname\": \"Alert2\",\n          \"namespace\": \"ns1\",\n          \"operation\": \"update\"\n        },\n        \"annotations\": {},\n        \"state\": \"firing\",\n        \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n        \"value\": \"0e+00\"\n      },\n      {\n        \"labels\": {\n          \"alertname\": \"Alert2\",\n          \"namespace\": \"ns1\",\n          \"operation\": \"delete\"\n        },\n        \"annotations\": {},\n        \"state\": \"firing\",\n        \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n        \"value\": \"0e+00\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "injectproxy/testdata/alerts_match_namespace_ns2.golden",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {\n    \"alerts\": [\n      {\n        \"labels\": {\n          \"alertname\": \"Alert3\",\n          \"namespace\": \"ns2\"\n        },\n        \"annotations\": {},\n        \"state\": \"firing\",\n        \"activeAt\": \"2019-12-18T13:14:39.972915521+01:00\",\n        \"value\": \"0e+00\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "injectproxy/testdata/alerts_match_namespaces_ns1_and_ns2.golden",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {\n    \"alerts\": [\n      {\n        \"labels\": {\n          \"alertname\": \"Alert1\",\n          \"namespace\": \"ns1\"\n        },\n        \"annotations\": {},\n        \"state\": \"firing\",\n        \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n        \"value\": \"0e+00\"\n      },\n      {\n        \"labels\": {\n          \"alertname\": \"Alert2\",\n          \"namespace\": \"ns1\",\n          \"operation\": \"update\"\n        },\n        \"annotations\": {},\n        \"state\": \"firing\",\n        \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n        \"value\": \"0e+00\"\n      },\n      {\n        \"labels\": {\n          \"alertname\": \"Alert2\",\n          \"namespace\": \"ns1\",\n          \"operation\": \"delete\"\n        },\n        \"annotations\": {},\n        \"state\": \"firing\",\n        \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n        \"value\": \"0e+00\"\n      },\n      {\n        \"labels\": {\n          \"alertname\": \"Alert3\",\n          \"namespace\": \"ns2\"\n        },\n        \"annotations\": {},\n        \"state\": \"firing\",\n        \"activeAt\": \"2019-12-18T13:14:39.972915521+01:00\",\n        \"value\": \"0e+00\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "injectproxy/testdata/alerts_no_match.golden",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {\n    \"alerts\": []\n  }\n}"
  },
  {
    "path": "injectproxy/testdata/alerts_no_namespace_error.golden",
    "content": "{\"error\":\"The \\\"namespace\\\" query parameter must be provided.\",\"errorType\":\"prom-label-proxy\",\"status\":\"error\"}\n"
  },
  {
    "path": "injectproxy/testdata/alerts_upstream_error.golden",
    "content": "error"
  },
  {
    "path": "injectproxy/testdata/rules_incomplete_upstream_response.golden",
    "content": ""
  },
  {
    "path": "injectproxy/testdata/rules_invalid_upstream_response.golden",
    "content": ""
  },
  {
    "path": "injectproxy/testdata/rules_match_namespace_ns1.golden",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {\n    \"groups\": [\n      {\n        \"name\": \"group1\",\n        \"file\": \"testdata/rules1.yml\",\n        \"rules\": [\n          {\n            \"name\": \"metric1\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns1\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214303,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.403557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"1\",\n            \"labels\": {\n              \"namespace\": \"ns1\",\n              \"operation\": \"create\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:53.403557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns1\",\n              \"operation\": \"update\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:54.403557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns1\",\n              \"operation\": \"delete\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:53.603557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"state\": \"firing\",\n            \"name\": \"Alert1\",\n            \"query\": \"metric1{namespace=\\\"ns1\\\"} == 0\",\n            \"duration\": 0,\n            \"keepFiringFor\": 0,\n            \"labels\": {\n              \"namespace\": \"ns1\"\n            },\n            \"annotations\": {},\n            \"alerts\": [\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert1\",\n                  \"namespace\": \"ns1\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n                \"value\": \"0e+00\"\n              }\n            ],\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:53.803557247+02:00\",\n            \"type\": \"alerting\"\n          },\n          {\n            \"state\": \"firing\",\n            \"name\": \"Alert2\",\n            \"query\": \"metric2{namespace=\\\"ns1\\\"} == 0\",\n            \"duration\": 0,\n            \"keepFiringFor\": 0,\n            \"labels\": {\n              \"namespace\": \"ns1\"\n            },\n            \"annotations\": {},\n            \"alerts\": [\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert2\",\n                  \"namespace\": \"ns1\",\n                  \"operation\": \"update\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n                \"value\": \"0e+00\"\n              },\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert2\",\n                  \"namespace\": \"ns1\",\n                  \"operation\": \"delete\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n                \"value\": \"0e+00\"\n              }\n            ],\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:53.903557247+02:00\",\n            \"type\": \"alerting\"\n          }\n        ],\n        \"interval\": 10\n      }\n    ]\n  }\n}"
  },
  {
    "path": "injectproxy/testdata/rules_match_namespace_ns2.golden",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {\n    \"groups\": [\n      {\n        \"name\": \"group1\",\n        \"file\": \"testdata/rules2.yml\",\n        \"rules\": [\n          {\n            \"name\": \"metric1\",\n            \"query\": \"1\",\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214303,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.403557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"state\": \"inactive\",\n            \"name\": \"Alert1\",\n            \"query\": \"metric1{namespace=\\\"ns2\\\"} == 0\",\n            \"duration\": 0,\n            \"keepFiringFor\": 0,\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"annotations\": {},\n            \"alerts\": [],\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.503557247+02:00\",\n            \"type\": \"alerting\"\n          }\n        ],\n        \"interval\": 10\n      },\n      {\n        \"name\": \"group2\",\n        \"file\": \"testdata/rules2.yml\",\n        \"rules\": [\n          {\n            \"name\": \"metric2\",\n            \"query\": \"1\",\n            \"labels\": {\n              \"namespace\": \"ns2\",\n              \"operation\": \"create\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.503557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"2\",\n            \"labels\": {\n              \"namespace\": \"ns2\",\n              \"operation\": \"update\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.603557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"3\",\n            \"labels\": {\n              \"namespace\": \"ns2\",\n              \"operation\": \"delete\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.643557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"name\": \"metric3\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.683557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"state\": \"inactive\",\n            \"name\": \"Alert2\",\n            \"query\": \"metric2{namespace=\\\"ns2\\\"} == 0\",\n            \"duration\": 0,\n            \"keepFiringFor\": 0,\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"annotations\": {},\n            \"alerts\": [],\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.803557247+02:00\",\n            \"type\": \"alerting\"\n          },\n          {\n            \"state\": \"firing\",\n            \"name\": \"Alert3\",\n            \"query\": \"metric3{namespace=\\\"ns2\\\"} == 0\",\n            \"duration\": 0,\n            \"keepFiringFor\": 0,\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"annotations\": {},\n            \"alerts\": [\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert3\",\n                  \"namespace\": \"ns2\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:39.972915521+01:00\",\n                \"value\": \"0e+00\"\n              }\n            ],\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.903557247+02:00\",\n            \"type\": \"alerting\"\n          }\n        ],\n        \"interval\": 10\n      }\n    ]\n  }\n}"
  },
  {
    "path": "injectproxy/testdata/rules_match_namespaces_ns1_and_ns2.golden",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {\n    \"groups\": [\n      {\n        \"name\": \"group1\",\n        \"file\": \"testdata/rules1.yml\",\n        \"rules\": [\n          {\n            \"name\": \"metric1\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns1\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214303,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.403557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"1\",\n            \"labels\": {\n              \"namespace\": \"ns1\",\n              \"operation\": \"create\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:53.403557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns1\",\n              \"operation\": \"update\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:54.403557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns1\",\n              \"operation\": \"delete\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:53.603557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"state\": \"firing\",\n            \"name\": \"Alert1\",\n            \"query\": \"metric1{namespace=\\\"ns1\\\"} == 0\",\n            \"duration\": 0,\n            \"keepFiringFor\": 0,\n            \"labels\": {\n              \"namespace\": \"ns1\"\n            },\n            \"annotations\": {},\n            \"alerts\": [\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert1\",\n                  \"namespace\": \"ns1\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n                \"value\": \"0e+00\"\n              }\n            ],\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:53.803557247+02:00\",\n            \"type\": \"alerting\"\n          },\n          {\n            \"state\": \"firing\",\n            \"name\": \"Alert2\",\n            \"query\": \"metric2{namespace=\\\"ns1\\\"} == 0\",\n            \"duration\": 0,\n            \"keepFiringFor\": 0,\n            \"labels\": {\n              \"namespace\": \"ns1\"\n            },\n            \"annotations\": {},\n            \"alerts\": [\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert2\",\n                  \"namespace\": \"ns1\",\n                  \"operation\": \"update\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n                \"value\": \"0e+00\"\n              },\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert2\",\n                  \"namespace\": \"ns1\",\n                  \"operation\": \"delete\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:44.543981127+01:00\",\n                \"value\": \"0e+00\"\n              }\n            ],\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:53.903557247+02:00\",\n            \"type\": \"alerting\"\n          }\n        ],\n        \"interval\": 10\n      },\n      {\n        \"name\": \"group1\",\n        \"file\": \"testdata/rules2.yml\",\n        \"rules\": [\n          {\n            \"name\": \"metric1\",\n            \"query\": \"1\",\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214303,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.403557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"state\": \"inactive\",\n            \"name\": \"Alert1\",\n            \"query\": \"metric1{namespace=\\\"ns2\\\"} == 0\",\n            \"duration\": 0,\n            \"keepFiringFor\": 0,\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"annotations\": {},\n            \"alerts\": [],\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.503557247+02:00\",\n            \"type\": \"alerting\"\n          }\n        ],\n        \"interval\": 10\n      },\n      {\n        \"name\": \"group2\",\n        \"file\": \"testdata/rules2.yml\",\n        \"rules\": [\n          {\n            \"name\": \"metric2\",\n            \"query\": \"1\",\n            \"labels\": {\n              \"namespace\": \"ns2\",\n              \"operation\": \"create\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.503557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"2\",\n            \"labels\": {\n              \"namespace\": \"ns2\",\n              \"operation\": \"update\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.603557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"name\": \"metric2\",\n            \"query\": \"3\",\n            \"labels\": {\n              \"namespace\": \"ns2\",\n              \"operation\": \"delete\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.643557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"name\": \"metric3\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.683557247+02:00\",\n            \"type\": \"recording\"\n          },\n          {\n            \"state\": \"inactive\",\n            \"name\": \"Alert2\",\n            \"query\": \"metric2{namespace=\\\"ns2\\\"} == 0\",\n            \"duration\": 0,\n            \"keepFiringFor\": 0,\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"annotations\": {},\n            \"alerts\": [],\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.803557247+02:00\",\n            \"type\": \"alerting\"\n          },\n          {\n            \"state\": \"firing\",\n            \"name\": \"Alert3\",\n            \"query\": \"metric3{namespace=\\\"ns2\\\"} == 0\",\n            \"duration\": 0,\n            \"keepFiringFor\": 0,\n            \"labels\": {\n              \"namespace\": \"ns2\"\n            },\n            \"annotations\": {},\n            \"alerts\": [\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert3\",\n                  \"namespace\": \"ns2\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:39.972915521+01:00\",\n                \"value\": \"0e+00\"\n              }\n            ],\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.903557247+02:00\",\n            \"type\": \"alerting\"\n          }\n        ],\n        \"interval\": 10\n      }\n    ]\n  }\n}"
  },
  {
    "path": "injectproxy/testdata/rules_no_match.golden",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {\n    \"groups\": []\n  }\n}"
  },
  {
    "path": "injectproxy/testdata/rules_no_match_with_gzip_not_requested.golden",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {\n    \"groups\": []\n  }\n}"
  },
  {
    "path": "injectproxy/testdata/rules_no_match_with_gzip_requested.golden",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {\n    \"groups\": []\n  }\n}"
  },
  {
    "path": "injectproxy/testdata/rules_no_namespace_error.golden",
    "content": "{\"error\":\"The \\\"namespace\\\" query parameter must be provided.\",\"errorType\":\"prom-label-proxy\",\"status\":\"error\"}\n"
  },
  {
    "path": "injectproxy/testdata/rules_upstream_error.golden",
    "content": "error"
  },
  {
    "path": "injectproxy/testdata/rules_with_active_alerts.golden",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {\n    \"groups\": [\n      {\n        \"name\": \"group3\",\n        \"file\": \"testdata/rules3.yml\",\n        \"rules\": [\n          {\n            \"state\": \"pending\",\n            \"name\": \"Alert3\",\n            \"query\": \"metric4{ns!=\\\"default\\\"} == 0\",\n            \"duration\": 300,\n            \"keepFiringFor\": 0,\n            \"labels\": {},\n            \"annotations\": {},\n            \"alerts\": [\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert3\",\n                  \"namespace\": \"ns3\"\n                },\n                \"annotations\": {},\n                \"state\": \"pending\",\n                \"activeAt\": \"2019-12-18T13:20:39.972915521+01:00\",\n                \"value\": \"0e+00\"\n              }\n            ],\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.903557247+02:00\",\n            \"type\": \"alerting\"\n          },\n          {\n            \"state\": \"firing\",\n            \"name\": \"Alert4\",\n            \"query\": \"metric5 == 0\",\n            \"duration\": 300,\n            \"keepFiringFor\": 0,\n            \"labels\": {},\n            \"annotations\": {},\n            \"alerts\": [\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert4\",\n                  \"namespace\": \"ns3\",\n                  \"state\": \"foo\"\n                },\n                \"annotations\": {},\n                \"state\": \"pending\",\n                \"activeAt\": \"2019-12-18T13:20:39.972915521+01:00\",\n                \"value\": \"0e+00\"\n              },\n              {\n                \"labels\": {\n                  \"alertname\": \"Alert1\",\n                  \"namespace\": \"ns3\",\n                  \"state\": \"bar\"\n                },\n                \"annotations\": {},\n                \"state\": \"firing\",\n                \"activeAt\": \"2019-12-18T13:14:39.972915521+01:00\",\n                \"value\": \"0e+00\"\n              }\n            ],\n            \"health\": \"ok\",\n            \"evaluationTime\": 0.000214,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.903557247+02:00\",\n            \"type\": \"alerting\"\n          }\n        ],\n        \"interval\": 10\n      }\n    ]\n  }\n}"
  },
  {
    "path": "injectproxy/testdata/rules_with_label_matchers.golden",
    "content": "{\n  \"status\": \"success\",\n  \"data\": {\n    \"groups\": [\n      {\n        \"name\": \"group1\",\n        \"file\": \"testdata/rules1.yml\",\n        \"rules\": [\n          {\n            \"name\": \"metric1\",\n            \"query\": \"0\",\n            \"labels\": {\n              \"namespace\": \"ns1\"\n            },\n            \"health\": \"ok\",\n            \"type\": \"recording\",\n            \"evaluationTime\": 0.000214303,\n            \"lastEvaluation\": \"2024-04-29T14:23:52.403557247+02:00\"\n          }\n        ],\n        \"interval\": 10\n      }\n    ]\n  }\n}"
  },
  {
    "path": "injectproxy/utils.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage injectproxy\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"net/http\"\n)\n\nfunc prometheusAPIError(w http.ResponseWriter, errorMessage string, code int) {\n\tw.Header().Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\tw.Header().Set(\"X-Content-Type-Options\", \"nosniff\")\n\tw.WriteHeader(code)\n\n\tres := map[string]string{\"status\": \"error\", \"errorType\": \"prom-label-proxy\", \"error\": errorMessage}\n\n\tif err := json.NewEncoder(w).Encode(res); err != nil {\n\t\tlog.Printf(\"error: Failed to encode json: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "main.go",
    "content": "// Copyright 2020 The Prometheus Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/metalmatze/signal/internalserver\"\n\t\"github.com/oklog/run\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/collectors\"\n\n\t\"github.com/prometheus-community/prom-label-proxy/injectproxy\"\n)\n\ntype arrayFlags []string\n\n// String is the method to format the flag's value, part of the flag.Value interface.\n// The String method's output will be used in diagnostics.\nfunc (i *arrayFlags) String() string {\n\treturn fmt.Sprint(*i)\n}\n\n// Set is the method to set the flag value, part of the flag.Value interface.\nfunc (i *arrayFlags) Set(value string) error {\n\tif value == \"\" {\n\t\treturn nil\n\t}\n\n\t*i = append(*i, value)\n\treturn nil\n}\n\nfunc main() {\n\tvar (\n\t\tinsecureListenAddress           string\n\t\tinternalListenAddress           string\n\t\tupstream                        string\n\t\tupstreamCaCert                  string\n\t\tqueryParam                      string\n\t\theaderName                      string\n\t\tlabel                           string\n\t\tlabelValues                     arrayFlags\n\t\tenableLabelAPIs                 bool\n\t\tunsafePassthroughPaths          string // Comma-delimited string.\n\t\tinsecureSkipVerify              bool\n\t\terrorOnReplace                  bool\n\t\tregexMatch                      bool\n\t\theaderUsesListSyntax            bool\n\t\trulesWithActiveAlerts           bool\n\t\tlabelMatchersForRulesAPI        bool\n\t\tpromQLDurationExpressionParsing bool\n\t\tpromQLExperimentalFunctions     bool\n\t\tpromQLExtendedRangeSelectors    bool\n\t\tpromQLBinopFillModifiers        bool\n\t)\n\n\tflagset := flag.NewFlagSet(os.Args[0], flag.ExitOnError)\n\tflagset.StringVar(&insecureListenAddress, \"insecure-listen-address\", \"\", \"The address the prom-label-proxy HTTP server should listen on.\")\n\tflagset.StringVar(&internalListenAddress, \"internal-listen-address\", \"\", \"The address the internal prom-label-proxy HTTP server should listen on to expose metrics about itself.\")\n\tflagset.StringVar(&queryParam, \"query-param\", \"\", \"Name of the HTTP parameter that contains the tenant value. At most one of -query-param, -header-name and -label-value should be given. If the flag isn't defined and neither -header-name nor -label-value is set, it will default to the value of the -label flag.\")\n\tflagset.StringVar(&headerName, \"header-name\", \"\", \"Name of the HTTP header name that contains the tenant value. At most one of -query-param, -header-name and -label-value should be given.\")\n\tflagset.StringVar(&upstream, \"upstream\", \"\", \"The upstream URL to proxy to.\")\n\tflagset.StringVar(&upstreamCaCert, \"upstream-ca-cert\", \"\", \"The upstream ca certificate file.\")\n\tflagset.StringVar(&label, \"label\", \"\", \"The label name to enforce in all proxied PromQL queries.\")\n\tflagset.Var(&labelValues, \"label-value\", \"A fixed label value to enforce in all proxied PromQL queries. At most one of -query-param, -header-name and -label-value should be given. It can be repeated in which case the proxy will enforce the union of values.\")\n\tflagset.BoolVar(&enableLabelAPIs, \"enable-label-apis\", false, \"When specified proxy allows to inject label to label APIs like /api/v1/labels and /api/v1/label/<name>/values. \"+\n\t\t\"NOTE: Enable with care because filtering by matcher is not implemented in older versions of Prometheus (>= v2.24.0 required) and Thanos (>= v0.18.0 required, >= v0.23.0 recommended). If enabled and \"+\n\t\t\"any labels endpoint does not support selectors, the injected matcher will have no effect.\")\n\tflagset.StringVar(&unsafePassthroughPaths, \"unsafe-passthrough-paths\", \"\", \"Comma delimited allow list of exact HTTP path segments that should be allowed to hit upstream URL without any enforcement. \"+\n\t\t\"This option is checked after Prometheus APIs, you cannot override enforced API endpoints to be not enforced with this option. Use carefully as it can easily cause a data leak if the provided path is an important \"+\n\t\t\"API (like /api/v1/configuration) which isn't enforced by prom-label-proxy. NOTE: \\\"all\\\" matching paths like \\\"/\\\" or \\\"\\\" and regex are not allowed.\")\n\tflagset.BoolVar(&insecureSkipVerify, \"insecure-skip-verify\", false, \"When specified, the proxy will bypass validation of the server's TLS/SSL certificate.\")\n\tflagset.BoolVar(&errorOnReplace, \"error-on-replace\", false, \"When specified, the proxy will return HTTP status code 400 if the query already contains a label matcher that differs from the one the proxy would inject.\")\n\tflagset.BoolVar(&regexMatch, \"regex-match\", false, \"When specified, the tenant name is treated as a regular expression. In this case, only one tenant name should be provided.\")\n\tflagset.BoolVar(&headerUsesListSyntax, \"header-uses-list-syntax\", false, \"When specified, the header line value will be parsed as a comma-separated list. This allows a single tenant header line to specify multiple tenant names.\")\n\tflagset.BoolVar(&rulesWithActiveAlerts, \"rules-with-active-alerts\", false, \"When true, the proxy will return alerting rules with active alerts matching the tenant label even when the tenant label isn't present in the rule's labels.\")\n\tflagset.BoolVar(&labelMatchersForRulesAPI, \"enable-label-matchers-for-rules-api\", false, \"When true, the proxy uses label matchers when querying the /api/v1/rules endpoint. NOTE: Enable with care because filtering by label matcher is not implemented in older versions of Prometheus (>= 2.54.0 required) and Thanos (>= v0.25.0 required). If not implemented by upstream, the response will not be filtered accordingly.\")\n\tflagset.BoolVar(&promQLDurationExpressionParsing, \"enable-promql-duration-expression-parsing\", false, \"When true, the proxy supports arithmetic for durations in PromQL expressions.\")\n\tflagset.BoolVar(&promQLExperimentalFunctions, \"enable-promql-experimental-functions\", false, \"When true, the proxy supports experimental functions in PromQL expressions.\")\n\tflagset.BoolVar(&promQLExtendedRangeSelectors, \"enable-promql-extended-range-selectors\", false, \"When true, the proxy supports extended range selectors in PromQL expressions.\")\n\tflagset.BoolVar(&promQLBinopFillModifiers, \"enable-promql-binop-fill-modifiers\", false, \"When true, the proxy supports binary operation fill modifiers in PromQL expressions.\")\n\n\t//nolint: errcheck // Parse() will exit on error.\n\tflagset.Parse(os.Args[1:])\n\tif label == \"\" {\n\t\tlog.Fatalf(\"-label flag cannot be empty\")\n\t}\n\n\tif len(labelValues) == 0 && queryParam == \"\" && headerName == \"\" {\n\t\tqueryParam = label\n\t}\n\n\tif len(labelValues) > 0 {\n\t\tif queryParam != \"\" || headerName != \"\" {\n\t\t\tlog.Fatalf(\"at most one of -query-param, -header-name and -label-value must be set\")\n\t\t}\n\t} else if queryParam != \"\" && headerName != \"\" {\n\t\tlog.Fatalf(\"at most one of -query-param, -header-name and -label-value must be set\")\n\t}\n\n\tupstreamURL, err := url.Parse(upstream)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to build parse upstream URL: %v\", err)\n\t}\n\n\tif upstreamURL.Scheme != \"http\" && upstreamURL.Scheme != \"https\" {\n\t\tlog.Fatalf(\"Invalid scheme for upstream URL %q, only 'http' and 'https' are supported\", upstream)\n\t}\n\n\treg := prometheus.NewRegistry()\n\treg.MustRegister(\n\t\tcollectors.NewGoCollector(),\n\t\tcollectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),\n\t)\n\n\topts := []injectproxy.Option{injectproxy.WithPrometheusRegistry(reg)}\n\tif upstreamCaCert != \"\" {\n\t\topts = append(opts, injectproxy.WithUpstreamCaCert(upstreamCaCert))\n\t}\n\n\tif enableLabelAPIs {\n\t\topts = append(opts, injectproxy.WithEnabledLabelsAPI())\n\t}\n\n\tif len(unsafePassthroughPaths) > 0 {\n\t\topts = append(opts, injectproxy.WithPassthroughPaths(strings.Split(unsafePassthroughPaths, \",\")))\n\t}\n\n\tif insecureSkipVerify {\n\t\topts = append(opts, injectproxy.WithInsecureSkipVerify())\n\t}\n\n\tif errorOnReplace {\n\t\topts = append(opts, injectproxy.WithErrorOnReplace())\n\t}\n\n\tif rulesWithActiveAlerts {\n\t\topts = append(opts, injectproxy.WithActiveAlerts())\n\t}\n\n\tif labelMatchersForRulesAPI {\n\t\topts = append(opts, injectproxy.WithLabelMatchersForRulesAPI())\n\t}\n\n\tif regexMatch {\n\t\tif len(labelValues) > 0 {\n\t\t\tif len(labelValues) > 1 {\n\t\t\t\tlog.Fatalf(\"Regex match is limited to one label value\")\n\t\t\t}\n\n\t\t\tcompiledRegex, err := regexp.Compile(labelValues[0])\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"Invalid regexp: %v\", err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif compiledRegex.MatchString(\"\") {\n\t\t\t\tlog.Fatalf(\"Regex should not match empty string\")\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\topts = append(opts, injectproxy.WithRegexMatch())\n\t}\n\n\tif promQLDurationExpressionParsing {\n\t\topts = append(opts, injectproxy.WithPromqlDurationExpressionParsing())\n\t}\n\n\tif promQLExperimentalFunctions {\n\t\topts = append(opts, injectproxy.WithPromqlExperimentalFunctions())\n\t}\n\n\tif promQLExtendedRangeSelectors {\n\t\topts = append(opts, injectproxy.WithPromqlExtendedRangeSelectors())\n\t}\n\n\tif promQLBinopFillModifiers {\n\t\topts = append(opts, injectproxy.WithPromqlBinopFillModifiers())\n\t}\n\n\tvar extractLabeler injectproxy.ExtractLabeler\n\tswitch {\n\tcase len(labelValues) > 0:\n\t\textractLabeler = injectproxy.StaticLabelEnforcer(labelValues)\n\tcase queryParam != \"\":\n\t\textractLabeler = injectproxy.HTTPFormEnforcer{ParameterName: queryParam}\n\tcase headerName != \"\":\n\t\textractLabeler = injectproxy.HTTPHeaderEnforcer{Name: http.CanonicalHeaderKey(headerName), ParseListSyntax: headerUsesListSyntax}\n\t}\n\n\tvar g run.Group\n\t{\n\t\t// Run the insecure HTTP server.\n\t\troutes, err := injectproxy.NewRoutes(upstreamURL, label, extractLabeler, opts...)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Failed to create injectproxy Routes: %v\", err)\n\t\t}\n\n\t\tmux := http.NewServeMux()\n\t\tmux.Handle(\"/\", routes)\n\n\t\tl, err := net.Listen(\"tcp\", insecureListenAddress)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Failed to listen on insecure address: %v\", err)\n\t\t}\n\n\t\tsrv := &http.Server{Handler: mux}\n\n\t\tg.Add(func() error {\n\t\t\tlog.Printf(\"Listening insecurely on %v\", l.Addr())\n\t\t\tif err := srv.Serve(l); err != nil && err != http.ErrServerClosed {\n\t\t\t\tlog.Printf(\"Server stopped with %v\", err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t}, func(error) {\n\t\t\tsrv.Close()\n\t\t})\n\t}\n\n\tif internalListenAddress != \"\" {\n\t\t// Run the internal HTTP server.\n\t\th := internalserver.NewHandler(\n\t\t\tinternalserver.WithName(\"Internal prom-label-proxy API\"),\n\t\t\tinternalserver.WithPrometheusRegistry(reg),\n\t\t\tinternalserver.WithPProf(),\n\t\t)\n\t\t// Run the HTTP server.\n\t\tl, err := net.Listen(\"tcp\", internalListenAddress)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Failed to listen on internal address: %v\", err)\n\t\t}\n\n\t\tsrv := &http.Server{Handler: h}\n\n\t\tg.Add(func() error {\n\t\t\tlog.Printf(\"Listening on %v for metrics and pprof\", l.Addr())\n\t\t\tif err := srv.Serve(l); err != nil && err != http.ErrServerClosed {\n\t\t\t\tlog.Printf(\"Internal server stopped with %v\", err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t}, func(error) {\n\t\t\tsrv.Close()\n\t\t})\n\t}\n\n\tg.Add(run.SignalHandler(context.Background(), syscall.SIGINT, syscall.SIGTERM))\n\n\tif err := g.Run(); err != nil {\n\t\tif !errors.As(err, &run.SignalError{}) {\n\t\t\tlog.Printf(\"Server stopped with %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\tlog.Print(\"Caught signal; exiting gracefully...\")\n\t}\n}\n"
  }
]