├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_dev.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── codeql.yml │ ├── go.yml │ └── releaser.yml ├── .gitignore ├── .goreleaser.yaml ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── Taskfile.yaml ├── docker-compose.yml ├── docker ├── Dockerfile ├── aws_quota_exporter │ └── config.yml ├── grafana │ ├── dashboards │ │ ├── dashboard.yml │ │ ├── lambda-concurrency.json │ │ └── quotas.json │ ├── datasources │ │ └── datasources.yml │ └── grafana.ini ├── prometheus │ ├── prometheus.yml │ └── rules │ │ └── rules.yml └── tests.yaml ├── example └── config.yml ├── go.mod ├── go.sum ├── img └── grafana.png ├── kubernetes ├── helm │ ├── README.md │ ├── Taskfile.yaml │ └── aqe │ │ ├── .helmignore │ │ ├── Chart.lock │ │ ├── Chart.yaml │ │ ├── README.md │ │ ├── charts │ │ ├── grafana │ │ │ ├── .helmignore │ │ │ ├── Chart.yaml │ │ │ ├── README.md │ │ │ ├── ci │ │ │ │ ├── default-values.yaml │ │ │ │ ├── with-affinity-values.yaml │ │ │ │ ├── with-dashboard-json-values.yaml │ │ │ │ ├── with-dashboard-values.yaml │ │ │ │ ├── with-extraconfigmapmounts-values.yaml │ │ │ │ ├── with-image-renderer-values.yaml │ │ │ │ ├── with-nondefault-values.yaml │ │ │ │ ├── with-persistence.yaml │ │ │ │ └── with-sidecars-envvaluefrom-values.yaml │ │ │ ├── dashboards │ │ │ │ └── quotas.json │ │ │ ├── templates │ │ │ │ ├── NOTES.txt │ │ │ │ ├── _config.tpl │ │ │ │ ├── _helpers.tpl │ │ │ │ ├── _pod.tpl │ │ │ │ ├── clusterrole.yaml │ │ │ │ ├── clusterrolebinding.yaml │ │ │ │ ├── configSecret.yaml │ │ │ │ ├── configmap-dashboard-provider.yaml │ │ │ │ ├── configmap.yaml │ │ │ │ ├── dashboards-json-configmap.yaml │ │ │ │ ├── deployment.yaml │ │ │ │ ├── extra-manifests.yaml │ │ │ │ ├── headless-service.yaml │ │ │ │ ├── hpa.yaml │ │ │ │ ├── image-renderer-deployment.yaml │ │ │ │ ├── image-renderer-hpa.yaml │ │ │ │ ├── image-renderer-network-policy.yaml │ │ │ │ ├── image-renderer-service.yaml │ │ │ │ ├── image-renderer-servicemonitor.yaml │ │ │ │ ├── ingress.yaml │ │ │ │ ├── networkpolicy.yaml │ │ │ │ ├── poddisruptionbudget.yaml │ │ │ │ ├── podsecuritypolicy.yaml │ │ │ │ ├── pvc.yaml │ │ │ │ ├── role.yaml │ │ │ │ ├── rolebinding.yaml │ │ │ │ ├── route.yaml │ │ │ │ ├── secret-env.yaml │ │ │ │ ├── secret.yaml │ │ │ │ ├── service.yaml │ │ │ │ ├── serviceaccount.yaml │ │ │ │ ├── servicemonitor.yaml │ │ │ │ ├── statefulset.yaml │ │ │ │ └── tests │ │ │ │ │ ├── test-configmap.yaml │ │ │ │ │ ├── test-podsecuritypolicy.yaml │ │ │ │ │ ├── test-role.yaml │ │ │ │ │ ├── test-rolebinding.yaml │ │ │ │ │ ├── test-serviceaccount.yaml │ │ │ │ │ └── test.yaml │ │ │ └── values.yaml │ │ └── prometheus-27.5.1.tgz │ │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── hpa.yaml │ │ ├── ingress.yaml │ │ ├── secret.yaml │ │ ├── service.yaml │ │ ├── serviceaccount.yaml │ │ ├── servicemonitor.yaml │ │ └── tests │ │ │ └── test-connection.yaml │ │ └── values.yaml └── manifests │ ├── Taskfile.yaml │ ├── aqe │ ├── Taskfile.yaml │ ├── aws_quota_exporter.yml │ ├── grafana.yml │ └── prometheus.yml │ └── argocd │ ├── Taskfile.yaml │ └── install.yaml ├── main.go ├── main_test.go └── pkg ├── cache.go ├── cache_test.go ├── collector.go ├── collector_test.go ├── config.go ├── config_test.go ├── grouping.go ├── grouping_test.go ├── log.go ├── log_test.go ├── scrapper.go └── scrapper_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .github 2 | docker/grafana 3 | docker/prometheus 4 | Taskfile 5 | README 6 | .pre-commit* 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_dev.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Development 3 | about: Feature opened by developer 4 | title: "[Feature]: " 5 | labels: feature 6 | assignees: emylincon 7 | 8 | --- 9 | 10 | **Describe feature to implement** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Example** 14 | Add example code if applicable. 15 | ```golang 16 | package main 17 | import ( 18 | "github.com/emylincon/aws_quota_exporter/pkg" 19 | ) 20 | 21 | func main(){ 22 | pkg.GetRandomData() 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Linked Issue 2 | 5 | [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) 6 | 7 | closes # 8 | 9 | ## Describe changes 10 | 13 | 14 | 15 | ## Pull Request Checklist 16 | 17 | 22 | 23 | - [ ] Review the [Contributing guidelines](CODE_OF_CONDUCT.md) 24 | - [ ] Run `pre-commit run -a` on your branch 25 | - [ ] Update your branch `git merge main` 26 | - [ ] Update documentation 27 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["main"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["main"] 20 | schedule: 21 | - cron: "21 17 * * 4" 22 | 23 | env: 24 | FILES_TO_MONITOR: "yml go" 25 | 26 | jobs: 27 | detect: 28 | name: detect file changes 29 | runs-on: ubuntu-latest 30 | outputs: 31 | run: ${{ steps.diff.outputs.run }} 32 | steps: 33 | - uses: actions/checkout@v3 34 | id: checkout 35 | 36 | # --diff-filter=[(A|C|D|M|R|T|U|X|B)…​[*]] 37 | # Select only files that are Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R), have their type (i.e. regular file, symlink, submodule, …​) 38 | # changed (T), are Unmerged (U), are Unknown (X), or have had their pairing Broken (B). Any combination of the filter characters (including none) can be used. 39 | # When * (All-or-none) is added to the combination, all paths are selected if there is any file that matches other criteria in the comparison; 40 | # if there is no file that matches other criteria, nothing is selected. 41 | - name: file-changes 42 | id: diff 43 | run: | 44 | run="no" 45 | if [[ "${{ github.event_name }}" == "pull_request" ]];then 46 | git fetch 47 | include="${{ env.FILES_TO_MONITOR }}" 48 | for i in $(git --no-pager diff --name-only --diff-filter=ACMRT ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}); do 49 | extension=${i##*.} 50 | if [[ "$include" == *"$extension"* ]]; then 51 | run="yes" 52 | fi 53 | done 54 | fi 55 | echo "run=${run}" >> "$GITHUB_OUTPUT" 56 | analyze: 57 | name: Analyze 58 | needs: detect 59 | if: needs.detect.outputs.run == 'yes' 60 | runs-on: ubuntu-latest 61 | permissions: 62 | actions: read 63 | contents: read 64 | security-events: write 65 | 66 | strategy: 67 | fail-fast: false 68 | matrix: 69 | language: ["go"] 70 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 71 | # Use only 'java' to analyze code written in Java, Kotlin or both 72 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 73 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 74 | 75 | steps: 76 | - name: Checkout repository 77 | uses: actions/checkout@v3 78 | 79 | # Initializes the CodeQL tools for scanning. 80 | - name: Initialize CodeQL 81 | uses: github/codeql-action/init@v2 82 | with: 83 | languages: ${{ matrix.language }} 84 | # If you wish to specify custom queries, you can do so here or in a config file. 85 | # By default, queries listed here will override any specified in a config file. 86 | # Prefix the list here with "+" to use these queries and those in the config file. 87 | 88 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 89 | # queries: security-extended,security-and-quality 90 | 91 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 92 | # If this step fails, then you should remove it and run the build manually (see below) 93 | - name: Autobuild 94 | uses: github/codeql-action/autobuild@v2 95 | 96 | # ℹ️ Command-line programs to run using the OS shell. 97 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 98 | 99 | # If the Autobuild fails above, remove it and uncomment the following three lines. 100 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 101 | 102 | # - run: | 103 | # echo "Run, Build Application using script" 104 | # ./location_of_script_within_repo/buildscript.sh 105 | 106 | - name: Perform CodeQL Analysis 107 | uses: github/codeql-action/analyze@v2 108 | with: 109 | category: "/language:${{matrix.language}}" 110 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | branches: ["main"] 11 | 12 | env: 13 | tag_name: v2.0.1 14 | REPO: https://github.com/emylincon/aws_quota_exporter 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@v3 24 | with: 25 | go-version: 1.21 26 | 27 | - name: Build 28 | run: go build -v ./... 29 | 30 | - name: Test 31 | run: go test -v ./... 32 | -------------------------------------------------------------------------------- /.github/workflows/releaser.yml: -------------------------------------------------------------------------------- 1 | name: releaser 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | tags: ["*"] 7 | 8 | env: 9 | APP_NAME: aqe 10 | IMAGE_NAME: ugwuanyi/aqe 11 | TEST_IMAGE: aqe/test 12 | REPO: https://github.com/emylincon/aws_quota_exporter 13 | GHCR: ghcr.io/emylincon/aws_quota_exporter 14 | 15 | jobs: 16 | prebuild: 17 | name: prebuild 18 | runs-on: ubuntu-latest 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | permissions: 22 | contents: write 23 | outputs: 24 | tag_name: ${{ steps.get_tag_name.outputs.tag_name }} 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Bump version and push tag 28 | id: tag_version 29 | if: github.ref_type != 'tag' 30 | uses: rymndhng/release-on-push-action@v0.28.0 31 | with: 32 | bump_version_scheme: norelease 33 | use_github_release_notes: true 34 | tag_prefix: v 35 | - name: Get the tag name 36 | id: get_tag_name 37 | run: | 38 | echo "my-tag=${{ steps.tag_version.outputs.tag_name }}" 39 | if [[ "${{ github.ref_type }}" == "tag" ]]; then 40 | echo "tag_name=${{ github.ref }}" >> "$GITHUB_OUTPUT" 41 | else 42 | echo "tag_name=${{ steps.tag_version.outputs.tag_name }}" >> "$GITHUB_OUTPUT" 43 | fi 44 | 45 | docker: 46 | name: Build docker image 47 | needs: [prebuild] 48 | permissions: 49 | contents: write 50 | packages: write 51 | runs-on: ubuntu-latest 52 | env: 53 | REGISTRY_DOCKERHUB_ENABLED: ${{ secrets.DOCKERHUB_USERNAME != null }} 54 | REGISTRY_GITHUB_ENABLED: true 55 | steps: 56 | - uses: actions/checkout@v4 57 | 58 | - uses: actions/setup-go@v5 59 | with: 60 | go-version: ^1.21 61 | 62 | - name: Set up QEMU 63 | uses: docker/setup-qemu-action@v2 64 | 65 | - name: Set up Docker Buildx 66 | uses: docker/setup-buildx-action@v2 67 | 68 | - name: Set up docker testing tool 69 | run: curl -LO https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-amd64 && chmod +x container-structure-test-linux-amd64 && sudo mv container-structure-test-linux-amd64 /usr/local/bin/container-structure-test 70 | 71 | - name: Login to DockerHub Registry 72 | if: ${{ env.REGISTRY_DOCKERHUB_ENABLED == 'true' && needs.prebuild.outputs.tag_name != null }} 73 | run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin 74 | 75 | - name: Login to Github Container Registry 76 | if: ${{ env.REGISTRY_GITHUB_ENABLED == 'true' && needs.prebuild.outputs.tag_name != null }} 77 | uses: docker/login-action@v2 78 | with: 79 | registry: ghcr.io 80 | username: ${{ github.repository_owner }} 81 | password: ${{ secrets.GITHUB_TOKEN }} 82 | 83 | - name: Build Test image 84 | run: docker build -t ${{ env.TEST_IMAGE }} -f docker/Dockerfile . 85 | 86 | - name: Run Image tests 87 | run: container-structure-test test --config docker/tests.yaml --image ${{ env.TEST_IMAGE }} 88 | 89 | - name: Push to dockerhub 90 | if: ${{ env.REGISTRY_DOCKERHUB_ENABLED == 'true' && needs.prebuild.outputs.tag_name != null }} 91 | uses: docker/build-push-action@v6 92 | with: 93 | context: . 94 | file: "docker/Dockerfile" 95 | platforms: linux/amd64,linux/arm64,linux/arm/v7 96 | push: true 97 | build-args: | 98 | APP_VERSION=${{ needs.prebuild.outputs.tag_name }} 99 | APP_COMMIT=${{ github.sha }} 100 | tags: | 101 | "${{ env.IMAGE_NAME }}:latest" 102 | "${{ env.IMAGE_NAME }}:${{ needs.prebuild.outputs.tag_name }}" 103 | 104 | - name: Push to ghcr 105 | if: ${{ env.REGISTRY_GITHUB_ENABLED == 'true' && needs.prebuild.outputs.tag_name != null }} 106 | uses: docker/build-push-action@v6 107 | with: 108 | context: . 109 | file: "docker/Dockerfile" 110 | platforms: linux/amd64,linux/arm64,linux/arm/v7 111 | push: true 112 | labels: org.opencontainers.image.source=${{ env.REPO }},org.opencontainers.image.version=${{ needs.prebuild.outputs.tag_name }} 113 | tags: | 114 | "${{ env.GHCR }}/${{ env.APP_NAME }}:latest" 115 | "${{ env.GHCR }}/${{ env.APP_NAME }}:${{ needs.prebuild.outputs.tag_name }}" 116 | build-args: | 117 | APP_VERSION=${{ needs.prebuild.outputs.tag_name }} 118 | APP_COMMIT=${{ github.sha }} 119 | 120 | 121 | release-tag: 122 | name: Release tag 123 | needs: [prebuild] 124 | runs-on: ubuntu-latest 125 | steps: 126 | - uses: actions/checkout@v4 127 | - name: Create Release 128 | if: github.ref_type == 'tag' 129 | id: create_release 130 | uses: ghalactic/github-release-from-tag@v5 131 | env: 132 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 133 | with: 134 | generateReleaseNotes: "true" 135 | draft: false 136 | prerelease: false 137 | 138 | goreleaser: 139 | name: goreleaser 140 | needs: [prebuild] 141 | if: needs.prebuild.outputs.tag_name != null 142 | runs-on: ubuntu-latest 143 | permissions: 144 | contents: write 145 | steps: 146 | - name: Checkout 147 | uses: actions/checkout@v4 148 | with: 149 | fetch-depth: 0 150 | - name: Set up Go 151 | uses: actions/setup-go@v5 152 | with: 153 | go-version: stable 154 | - name: Run GoReleaser 155 | uses: goreleaser/goreleaser-action@v6 156 | with: 157 | distribution: goreleaser 158 | version: latest 159 | args: release --clean 160 | env: 161 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 162 | 163 | chartreleaser: 164 | name: Releaser Helm Chart 165 | needs: [prebuild, docker, goreleaser] 166 | if: needs.prebuild.outputs.tag_name != null 167 | # depending on default permission settings for your org (contents being read-only or read-write for workloads), you will have to add permissions 168 | # see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token 169 | permissions: 170 | contents: write 171 | runs-on: ubuntu-latest 172 | steps: 173 | - name: Checkout 174 | uses: actions/checkout@v4 175 | with: 176 | fetch-depth: 0 177 | token: ${{ secrets.GITHUB_TOKEN }} 178 | 179 | - name: Configure Git 180 | run: | 181 | git config user.name "$GITHUB_ACTOR" 182 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 183 | 184 | - name: Config Chart Helm Version 185 | run: | 186 | tag_name=${{ needs.prebuild.outputs.tag_name }} 187 | version="${tag_name#v}" 188 | configfile="kubernetes/helm/aqe/Chart.yaml" 189 | yq -i ".appVersion = \"${version}\"" $configfile 190 | yq -i ".version = \"${version}\"" $configfile 191 | cat $configfile 192 | 193 | - name: Update Image version 194 | run: | 195 | tag_name=${{ needs.prebuild.outputs.tag_name }} 196 | configfile="kubernetes/helm/aqe/values.yaml" 197 | yq -i ".image.tag = \"${tag_name}\"" $configfile 198 | cat $configfile 199 | 200 | # - name: Push Changes to main 201 | # run: | 202 | # branch="auto/releaser-pipeline${{ needs.prebuild.outputs.tag_name }}" 203 | # git checkout -b $branch 204 | # git commit -am "releaser (pipeline): Update Helm Chart version" 205 | # git push --set-upstream origin $branch 206 | 207 | # - name: Create Pull Request 208 | # uses: peter-evans/create-pull-request@v3.10.1 209 | # with: 210 | # token: ${{ secrets.GITHUB_TOKEN }} 211 | # commit-message: "releaser (pipeline): Update Helm Chart version ${{ needs.prebuild.outputs.tag_name }}" 212 | # title: "releaser (pipeline): Update Helm Chart version ${{ needs.prebuild.outputs.tag_name }}" 213 | # body: | 214 | # # Description 215 | # Update Helm Chart version ${{ needs.prebuild.outputs.tag_name }} 216 | # branch: auto/releaser-pipeline${{ needs.prebuild.outputs.tag_name }} 217 | # base: main 218 | 219 | - name: Install Helm 220 | uses: azure/setup-helm@v3 221 | 222 | - name: helm package 223 | run: | 224 | helm package kubernetes/helm/aqe -d .cr-release-packages/ 225 | helm repo index .cr-release-packages 226 | 227 | - name: Add new chart entry to index.yaml 228 | run: | 229 | tag_name=${{ needs.prebuild.outputs.tag_name }} 230 | version="${tag_name#v}" # without the v 231 | indexfile=".cr-release-packages/index.yaml" 232 | 233 | pkg=helm-chart-aqe-${version}.tgz 234 | mv .cr-release-packages/aqe-${version}.tgz .cr-release-packages/$pkg 235 | 236 | wget -q https://github.com/emylincon/aws_quota_exporter/raw/gh-pages/index.yaml 237 | 238 | # set chart remote URL 239 | chart="${{ env.REPO }}/releases/download/${tag_name}/${pkg}" 240 | 241 | # update chart URL 242 | yq -i ".entries.aqe[-1].urls[0] = \"${chart}\"" $indexfile 243 | 244 | # merge entries.aqe two index.yaml files 245 | yq eval-all '.entries.aqe as $item ireduce ({}; .entries.aqe += $item) | .entries.aqe' $indexfile index.yaml > merge_entries.yaml 246 | 247 | # update index.yaml with merged entries.aqe 248 | yq eval '.entries.aqe = load("merge_entries.yaml")' $indexfile > index.yaml 249 | 250 | # cleanup 251 | rm merge_entries.yaml 252 | 253 | # https://github.com/marketplace/actions/upload-to-github-release 254 | - name: git release update 255 | uses: xresloader/upload-to-github-release@v1 256 | env: 257 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 258 | with: 259 | file: ".cr-release-packages/*.tgz" 260 | update_latest_release: true 261 | # https://github.com/peaceiris/actions-gh-pages#table-of-contents 262 | 263 | - name: Deploy 264 | uses: peaceiris/actions-gh-pages@v4 265 | with: 266 | github_token: ${{ secrets.GITHUB_TOKEN }} 267 | exclude_assets: ".cr-release-packages" 268 | publish_dir: . 269 | enable_jekyll: true 270 | commit_message: "Tag ${{ needs.prebuild.outputs.tag_name }} release update" 271 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | .env 17 | 18 | dist/ 19 | logs/ 20 | packages/ 21 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | goos: 13 | - linux 14 | - windows 15 | - darwin 16 | 17 | archives: 18 | - format: tar.gz 19 | # this name template makes the OS and Arch compatible with the results of uname. 20 | name_template: >- 21 | {{ .ProjectName }}_ 22 | {{- title .Os }}_ 23 | {{- if eq .Arch "amd64" }}x86_64 24 | {{- else if eq .Arch "386" }}i386 25 | {{- else }}{{ .Arch }}{{ end }} 26 | {{- if .Arm }}v{{ .Arm }}{{ end }} 27 | # use zip for windows archives 28 | format_overrides: 29 | - goos: windows 30 | format: zip 31 | checksum: 32 | name_template: "checksums.txt" 33 | snapshot: 34 | name_template: "{{ incpatch .Version }}-next" 35 | changelog: 36 | sort: asc 37 | filters: 38 | exclude: 39 | - "^docs:" 40 | - "^test:" 41 | # The lines beneath this are called `modelines`. See `:help modeline` 42 | # Feel free to remove those if you don't want/use them. 43 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 44 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 45 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | exclude: "kubernetes/helm/aqe/templates" 7 | args: [--allow-multiple-documents] 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | - id: check-added-large-files 11 | - id: check-merge-conflict 12 | - id: mixed-line-ending 13 | - id: no-commit-to-branch 14 | - repo: https://github.com/psf/black 15 | rev: 21.12b0 16 | hooks: 17 | - id: black 18 | - repo: https://github.com/tekwizely/pre-commit-golang 19 | rev: v1.0.0-beta.5 20 | hooks: 21 | - id: go-build-mod 22 | - id: go-mod-tidy 23 | - id: go-test-mod 24 | - id: go-vet-mod 25 | - id: go-fmt 26 | - id: go-lint 27 | - repo: https://github.com/zricethezav/gitleaks 28 | rev: v8.2.0 29 | hooks: 30 | - id: gitleaks 31 | - repo: https://github.com/norwoodj/helm-docs 32 | rev: v1.11.0 33 | hooks: 34 | - id: helm-docs 35 | args: 36 | - --chart-search-root=kubernetes/helm 37 | - --template-files=README.md.gotmpl 38 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | emylincon@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Emeka E Ugwuanyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Taskfile.yaml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: "3" 4 | 5 | includes: 6 | k8: 7 | taskfile: ./kubernetes/manifests 8 | helm: 9 | taskfile: ./kubernetes/helm 10 | vars: 11 | VERSION: 0.0.0 12 | 13 | tasks: 14 | start: 15 | desc: "start application in dev mode" 16 | cmds: 17 | - export AWS_PROFILE=emeka;go run . --prom.port=10100 --config.file=example/config.yml --log.level=debug --log.format=text --cache.duration=1m --collect.usage --cache.serve-stale 18 | silent: true 19 | retest: 20 | desc: "go release build test" 21 | cmds: 22 | - goreleaser release --snapshot --clean 23 | silent: true 24 | fuzz: 25 | desc: "go fuzz test" 26 | cmds: 27 | - go test -fuzz=. 28 | silent: true 29 | test_verbose: 30 | desc: "run tests in verbose mode" 31 | cmds: 32 | - go test -v ./... 33 | silent: true 34 | test: 35 | desc: "run tests" 36 | cmds: 37 | - go test ./... 38 | silent: true 39 | compose-up: 40 | desc: "docker compose up" 41 | cmds: 42 | - docker-compose up -d --pull always 43 | compose-up-build: 44 | desc: "docker compose build up" 45 | cmds: 46 | - docker-compose up -d --build 47 | compose-down: 48 | desc: "docker compose down" 49 | cmds: 50 | - docker-compose down 51 | docker-build: 52 | desc: "build docker image" 53 | cmds: 54 | - | 55 | commit=$(git log --format="%H" -n 1) 56 | docker build --build-arg="APP_VERSION=dev" --build-arg="APP_COMMIT=${commit}" -t ugwuanyi/aqe:dev -f docker/Dockerfile . 57 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3.8' 3 | 4 | services: 5 | aws_quota_exporter: 6 | image: ugwuanyi/aqe:latest 7 | build: 8 | context: . 9 | dockerfile: docker/Dockerfile 10 | ports: 11 | - 10100:10100 12 | volumes: 13 | - ./docker/aws_quota_exporter/config.yml:/etc/aqe/config.yml 14 | - ${AWS_FOLDER}:/exporter/.aws/ 15 | environment: 16 | - AWS_PROFILE=${AWS_PROFILE} 17 | command: --log.level=debug --log.format=text --collect.usage --cache.duration=30m 18 | 19 | prometheus: 20 | image: prom/prometheus 21 | ports: 22 | - 9090:9090 23 | volumes: 24 | - ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml 25 | - ./docker/prometheus/rules:/etc/prometheus/rules 26 | 27 | grafana: 28 | image: grafana/grafana 29 | ports: 30 | - 3000:3000 31 | volumes: 32 | - ./docker/grafana/dashboards:/etc/grafana/provisioning/dashboards 33 | - ./docker/grafana/datasources:/etc/grafana/provisioning/datasources 34 | - ./docker/grafana/grafana.ini:/etc/grafana/grafana.ini 35 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | 3 | ARG APP_VERSION="0.0.0" 4 | ARG APP_COMMIT="latest" 5 | 6 | WORKDIR /opt 7 | 8 | COPY . . 9 | 10 | RUN go build -o aqe -ldflags="-X 'main.version=${APP_VERSION}' -X 'main.date=$(date "+%Y-%m-%dT%TZ")' -X 'main.commit=${APP_COMMIT}'" . 11 | 12 | FROM alpine:latest 13 | 14 | RUN apk update && apk --no-cache add ca-certificates && \ 15 | addgroup -g 1000 exporter && \ 16 | adduser -u 1000 -D -G exporter exporter -h /exporter 17 | 18 | WORKDIR /exporter 19 | 20 | COPY --from=builder /opt/aqe /usr/local/bin/aqe 21 | COPY ./docker/aws_quota_exporter/config.yml /etc/aqe/config.yml 22 | 23 | EXPOSE 10100 24 | 25 | USER exporter 26 | 27 | ENTRYPOINT [ "aqe" ] 28 | 29 | CMD ["--prom.port=10100"] 30 | -------------------------------------------------------------------------------- /docker/aws_quota_exporter/config.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - serviceCode: lambda 3 | regions: 4 | - us-west-2 5 | - us-east-2 6 | - serviceCode: cloudformation 7 | regions: 8 | - us-west-2 9 | - us-east-2 10 | - serviceCode: ec2 11 | regions: 12 | - us-west-2 13 | - us-east-2 14 | -------------------------------------------------------------------------------- /docker/grafana/dashboards/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'Prometheus' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | disableDeletion: false 9 | editable: true 10 | options: 11 | path: /etc/grafana/provisioning/dashboards 12 | -------------------------------------------------------------------------------- /docker/grafana/dashboards/lambda-concurrency.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "target": { 15 | "limit": 100, 16 | "matchAny": false, 17 | "tags": [], 18 | "type": "dashboard" 19 | }, 20 | "type": "dashboard" 21 | } 22 | ] 23 | }, 24 | "editable": true, 25 | "fiscalYearStartMonth": 0, 26 | "graphTooltip": 0, 27 | "id": 1, 28 | "links": [], 29 | "liveNow": false, 30 | "panels": [ 31 | { 32 | "datasource": { 33 | "type": "prometheus", 34 | "uid": "PBFA97CFB590B2093" 35 | }, 36 | "fieldConfig": { 37 | "defaults": { 38 | "color": { 39 | "mode": "thresholds" 40 | }, 41 | "mappings": [], 42 | "thresholds": { 43 | "mode": "absolute", 44 | "steps": [ 45 | { 46 | "color": "green", 47 | "value": null 48 | } 49 | ] 50 | } 51 | }, 52 | "overrides": [] 53 | }, 54 | "gridPos": { 55 | "h": 9, 56 | "w": 12, 57 | "x": 0, 58 | "y": 0 59 | }, 60 | "id": 2, 61 | "options": { 62 | "colorMode": "value", 63 | "graphMode": "area", 64 | "justifyMode": "auto", 65 | "orientation": "auto", 66 | "reduceOptions": { 67 | "calcs": [ 68 | "lastNotNull" 69 | ], 70 | "fields": "", 71 | "values": false 72 | }, 73 | "textMode": "auto" 74 | }, 75 | "pluginVersion": "", 76 | "targets": [ 77 | { 78 | "datasource": { 79 | "type": "prometheus", 80 | "uid": "PBFA97CFB590B2093" 81 | }, 82 | "editorMode": "builder", 83 | "expr": "sum by(region) (aws_quota_lambda_concurrent_executions)", 84 | "legendFormat": "__auto", 85 | "range": true, 86 | "refId": "A" 87 | } 88 | ], 89 | "title": "Lambda Execution concurrency", 90 | "type": "stat" 91 | } 92 | ], 93 | "schemaVersion": 36, 94 | "style": "dark", 95 | "tags": [], 96 | "templating": { 97 | "list": [] 98 | }, 99 | "time": { 100 | "from": "now-6h", 101 | "to": "now" 102 | }, 103 | "timepicker": {}, 104 | "timezone": "", 105 | "title": "Lambda Concurrency", 106 | "version": 1, 107 | "weekStart": "" 108 | } 109 | -------------------------------------------------------------------------------- /docker/grafana/datasources/datasources.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | # Access mode - proxy (server in the UI) or direct (browser in the UI). 7 | access: proxy 8 | url: http://prometheus:9090 9 | orgId: 1 10 | editable: true 11 | -------------------------------------------------------------------------------- /docker/grafana/grafana.ini: -------------------------------------------------------------------------------- 1 | [auth.basic] 2 | enabled = false 3 | 4 | [auth.grafana_com] 5 | enabled = false 6 | 7 | [auth.anonymous] 8 | enabled = true 9 | 10 | # Organization name that should be used for unauthenticated users 11 | org_name = Main Org. 12 | 13 | # Role for unauthenticated users, other valid values are `Viewer`, `Editor` and `Admin` 14 | org_role = Admin 15 | 16 | # Hide the Grafana version text from the footer and help tooltip for unauthenticated users (default: false) 17 | hide_version = true 18 | -------------------------------------------------------------------------------- /docker/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s # By default, scrape targets every 15 seconds. 3 | 4 | # A scrape configuration containing exactly one endpoint to scrape: 5 | # Here it's Prometheus itself. 6 | scrape_configs: 7 | # The job name is added as a label `job=` to any timeseries scraped from this config. 8 | - job_name: "prometheus" 9 | 10 | # Override the global default and scrape targets from this job every 5 seconds. 11 | scrape_interval: 5s 12 | 13 | static_configs: 14 | - targets: ["localhost:9090"] 15 | 16 | - job_name: "aws_quota_exporter" 17 | 18 | # Override the global default and scrape targets from this job every 5 seconds. 19 | scrape_interval: 15s 20 | 21 | static_configs: 22 | - targets: ["aws_quota_exporter:10100"] 23 | -------------------------------------------------------------------------------- /docker/prometheus/rules/rules.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: sample-alerts 3 | rules: 4 | - alert: App1Slow 5 | expr: 1 == 0 6 | labels: 7 | severity: warning 8 | service: app1 9 | annotations: 10 | summary: App 1 is running slow 11 | -------------------------------------------------------------------------------- /docker/tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | schemaVersion: 2.0.0 3 | 4 | fileExistenceTests: 5 | - name: "aqe bin executable exists" 6 | path: "/usr/local/bin/aqe" 7 | shouldExist: true 8 | 9 | - name: "config file exists" 10 | path: "/etc/aqe/config.yml" 11 | shouldExist: true 12 | 13 | commandTests: 14 | - name: "aqe is installed" 15 | command: "which" 16 | args: ["aqe"] 17 | expectedOutput: ["/usr/local/bin/aq"] 18 | - name: "aqe help" 19 | command: "aqe" 20 | args: ["-h"] 21 | excludedOutput: ["Usage of aqe.*"] 22 | 23 | metadataTest: 24 | exposedPorts: ["10100"] 25 | workdir: "/exporter" 26 | user: exporter 27 | entrypoint: ["aqe"] 28 | cmd: ["--prom.port=10100"] 29 | # https://semaphoreci.com/blog/structure-testing-for-docker-containers 30 | # https://github.com/GoogleContainerTools/container-structure-test 31 | # container-structure-test test --config docker/test.yml --image ugwuanyi/aqe:latest 32 | -------------------------------------------------------------------------------- /example/config.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - serviceCode: lambda 3 | accountName: dev-account 4 | regions: 5 | - us-west-1 6 | - us-east-1 7 | - serviceCode: cloudtrail 8 | accountName: dev-account 9 | regions: 10 | - eu-central-1 11 | - serviceCode: ebs 12 | accountName: dev-account 13 | regions: 14 | - eu-central-1 15 | - serviceCode: cloudformation 16 | accountName: dev-account 17 | regions: 18 | - us-west-1 19 | - us-east-1 20 | - serviceCode: rds 21 | accountName: dev-account 22 | regions: 23 | - us-west-1 24 | - us-east-1 25 | - serviceCode: ec2 26 | accountName: dev-account 27 | regions: 28 | - us-west-1 29 | - us-east-1 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emylincon/aws_quota_exporter 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/adrg/strutil v0.3.1 7 | github.com/aws/aws-sdk-go v1.44.245 8 | github.com/aws/aws-sdk-go-v2 v1.36.1 9 | github.com/aws/aws-sdk-go-v2/config v1.29.6 10 | github.com/aws/aws-sdk-go-v2/credentials v1.17.59 11 | github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.43.14 12 | github.com/aws/aws-sdk-go-v2/service/servicequotas v1.25.18 13 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 14 | github.com/emylincon/golist v1.4.5 15 | github.com/prometheus/client_golang v1.14.0 16 | github.com/prometheus/common v0.37.0 17 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 18 | gopkg.in/yaml.v2 v2.4.0 19 | ) 20 | 21 | require ( 22 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 // indirect 23 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 // indirect 24 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 // indirect 25 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect 26 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect 27 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 // indirect 30 | github.com/aws/smithy-go v1.22.2 // indirect 31 | github.com/beorn7/perks v1.0.1 // indirect 32 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 33 | github.com/golang/protobuf v1.5.2 // indirect 34 | github.com/jmespath/go-jmespath v0.4.0 // indirect 35 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 36 | github.com/prometheus/client_model v0.3.0 // indirect 37 | github.com/prometheus/procfs v0.8.0 // indirect 38 | golang.org/x/sys v0.1.0 // indirect 39 | google.golang.org/protobuf v1.28.1 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /img/grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emylincon/aws_quota_exporter/82caffb48439387e4e9714b3cc6a0e45066e5d9c/img/grafana.png -------------------------------------------------------------------------------- /kubernetes/helm/README.md: -------------------------------------------------------------------------------- 1 | # AWS Quota Exporter Helm Chart 2 | 3 | This Helm chart deploys the AWS Quota Exporter on a Kubernetes cluster. 4 | 5 | ## Prerequisites 6 | 7 | - Kubernetes 1.12+ 8 | - Helm 3.0+ 9 | 10 | ## Installation 11 | 12 | 1. Add the Helm repository: 13 | 14 | ```sh 15 | helm repo add aws-quota-exporter https://emylincon.github.io/aws_quota_exporter 16 | helm repo update 17 | ``` 18 | 19 | 2. Install the chart with the release name `aqe`: 20 | 21 | ```sh 22 | helm install aqe aws-quota-exporter/aqe 23 | ``` 24 | 25 | ## Uninstallation 26 | 27 | To uninstall/delete the `aqe` deployment: 28 | 29 | ```sh 30 | helm uninstall aqe 31 | ``` 32 | 33 | ## Chart dependencies 34 | ### Prometheus 35 | The prometheus chart dependency is managed [here](https://github.com/prometheus-community/helm-charts) 36 | 37 | ### Grafana 38 | The grafana chart dependency is managed [here](https://github.com/grafana/helm-charts/tree/main/charts/grafana) 39 | -------------------------------------------------------------------------------- /kubernetes/helm/Taskfile.yaml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: "3" 4 | 5 | vars: 6 | VERSION: '{{.VERSION | default "0.0.0"}}' 7 | 8 | tasks: 9 | up: 10 | desc: "package and deploy helm chart" 11 | cmds: 12 | - helm dependency update kubernetes/helm/aqe 13 | - helm package kubernetes/helm/aqe -d packages/ --version {{.VERSION}} 14 | - helm install -f values.test aqe packages/aqe-{{.VERSION}}.tgz 15 | silent: true 16 | remote: 17 | desc: "install using remote chart" 18 | cmds: 19 | - helm repo update aws_quota_exporter 20 | - helm install -n aqe -f values.test aqe aws_quota_exporter/aqe 21 | status: 22 | desc: helm status 23 | cmds: 24 | - helm status aqe 25 | down: 26 | desc: delete helm chart 27 | cmds: 28 | - helm uninstall aqe 29 | expose-ingress: 30 | desc: expose minikube ingress. it is exposed by default in 127.0.0.1 31 | cmds: 32 | - minikube tunnel 33 | port-forward-aqe: 34 | desc: port forward to aqe services localhost 35 | cmds: 36 | - kubectl -n default port-forward svc/aqe 10100:10100 & 37 | - kubectl -n default port-forward svc/aqe-grafana 3000:80 & 38 | - kubectl -n default port-forward svc/aqe-prometheus-server 9090:80 & 39 | grafana-secret: 40 | desc: get grafana secret 41 | cmds: 42 | - kubectl get secrets aqe-grafana -o jsonpath="{.data}" | jq -r 'to_entries[] | "\(.key)=\(.value | @base64d)"' 43 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: prometheus 3 | repository: https://prometheus-community.github.io/helm-charts 4 | version: 27.5.1 5 | - name: grafana 6 | repository: file://charts/grafana 7 | version: 8.10.1 8 | digest: sha256:903833ddb5756c259472ac00e70c1133cdae19ce13c719dba19a145baea46f4d 9 | generated: "2025-03-09T18:52:07.898354Z" 10 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: aqe 3 | description: A Helm chart for aws quota exporter 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 2.0.2 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "2.0.2" 25 | dependencies: 26 | - name: prometheus 27 | repository: "https://prometheus-community.github.io/helm-charts" 28 | version: 27.5.1 29 | condition: prometheus.enabled 30 | - name: grafana 31 | repository: "file://charts/grafana" 32 | version: 8.10.1 33 | condition: grafana.enabled 34 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/README.md: -------------------------------------------------------------------------------- 1 | # aqe 2 | 3 | ![Version: 2.0.2](https://img.shields.io/badge/Version-2.0.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.0.2](https://img.shields.io/badge/AppVersion-2.0.2-informational?style=flat-square) 4 | 5 | A Helm chart for aws quota exporter 6 | 7 | ## Requirements 8 | 9 | | Repository | Name | Version | 10 | |------------|------|---------| 11 | | file://charts/grafana | grafana | 8.10.1 | 12 | | https://prometheus-community.github.io/helm-charts | prometheus | 27.5.1 | 13 | 14 | ## Values 15 | 16 | | Key | Type | Default | Description | 17 | |-----|------|---------|-------------| 18 | | affinity | object | `{}` | | 19 | | autoscaling.enabled | bool | `false` | | 20 | | autoscaling.maxReplicas | int | `100` | | 21 | | autoscaling.minReplicas | int | `1` | | 22 | | autoscaling.targetCPUUtilizationPercentage | int | `80` | | 23 | | configmap."config.yml" | string | `"jobs:\n - serviceCode: lambda\n regions:\n - us-west-2\n - us-east-2\n - serviceCode: cloudformation\n regions:\n - us-west-2\n - us-east-2\n - serviceCode: ec2\n regions:\n - us-west-2\n - us-east-2\n"` | | 24 | | env.AWS_REGION | string | `"us-west-2"` | | 25 | | fullnameOverride | string | `""` | | 26 | | grafana."grafana.ini"."auth.anonymous".enabled | bool | `true` | | 27 | | grafana."grafana.ini"."auth.basic".enabled | bool | `false` | | 28 | | grafana."grafana.ini"."auth.grafana_com".enabled | bool | `false` | | 29 | | grafana."grafana.ini".hide_version | bool | `true` | | 30 | | grafana."grafana.ini".org_name | bool | `true` | | 31 | | grafana."grafana.ini".org_role | string | `"Admin"` | | 32 | | grafana.dashboardProviders."dashboardproviders.yaml".apiVersion | int | `1` | | 33 | | grafana.dashboardProviders."dashboardproviders.yaml".providers[0].disableDeletion | bool | `false` | | 34 | | grafana.dashboardProviders."dashboardproviders.yaml".providers[0].editable | bool | `true` | | 35 | | grafana.dashboardProviders."dashboardproviders.yaml".providers[0].folder | string | `""` | | 36 | | grafana.dashboardProviders."dashboardproviders.yaml".providers[0].name | string | `"prometheus"` | | 37 | | grafana.dashboardProviders."dashboardproviders.yaml".providers[0].options.path | string | `"/var/lib/grafana/dashboards/prometheus"` | | 38 | | grafana.dashboardProviders."dashboardproviders.yaml".providers[0].orgId | int | `1` | | 39 | | grafana.dashboardProviders."dashboardproviders.yaml".providers[0].type | string | `"file"` | | 40 | | grafana.dashboards.prometheus.quotas.file | string | `"dashboards/quotas.json"` | | 41 | | grafana.datasources."datasources.yaml".apiVersion | int | `1` | | 42 | | grafana.datasources."datasources.yaml".datasources[0].access | string | `"proxy"` | | 43 | | grafana.datasources."datasources.yaml".datasources[0].editable | bool | `true` | | 44 | | grafana.datasources."datasources.yaml".datasources[0].name | string | `"prometheus"` | | 45 | | grafana.datasources."datasources.yaml".datasources[0].orgId | int | `1` | | 46 | | grafana.datasources."datasources.yaml".datasources[0].type | string | `"prometheus"` | | 47 | | grafana.datasources."datasources.yaml".datasources[0].url | string | `"http://aqe-prometheus-server.default.svc.cluster.local:80"` | | 48 | | grafana.enabled | bool | `true` | | 49 | | image.pullPolicy | string | `"IfNotPresent"` | | 50 | | image.repository | string | `"ugwuanyi/aqe"` | | 51 | | image.tag | string | `"latest"` | | 52 | | imagePullSecrets | list | `[]` | | 53 | | ingress.annotations | object | `{}` | | 54 | | ingress.className | string | `""` | | 55 | | ingress.enabled | bool | `false` | | 56 | | ingress.hosts[0].host | string | `"aqe.chart.emylincon.com"` | | 57 | | ingress.hosts[0].paths[0].path | string | `"/"` | | 58 | | ingress.hosts[0].paths[0].pathType | string | `"Prefix"` | | 59 | | ingress.tls | list | `[]` | | 60 | | livenessProbe.httpGet.path | string | `"/"` | | 61 | | livenessProbe.httpGet.port | string | `"http"` | | 62 | | livenessProbe.initialDelaySeconds | int | `60` | | 63 | | nameOverride | string | `""` | | 64 | | nodeSelector | object | `{}` | | 65 | | podAnnotations | object | `{}` | | 66 | | podArgs[0] | string | `"--log.level=info"` | | 67 | | podArgs[1] | string | `"--log.format=text"` | | 68 | | podSecurityContext | object | `{}` | | 69 | | prometheus.alertmanager.enabled | bool | `false` | | 70 | | prometheus.enabled | bool | `true` | | 71 | | prometheus.kube-state-metrics.enabled | bool | `false` | | 72 | | prometheus.prometheus-node-exporter.enabled | bool | `false` | | 73 | | prometheus.prometheus-pushgateway.enabled | bool | `false` | | 74 | | prometheus.serverFiles."prometheus.yml".scrape_configs[0].job_name | string | `"prometheus"` | | 75 | | prometheus.serverFiles."prometheus.yml".scrape_configs[0].scrape_interval | string | `"5s"` | | 76 | | prometheus.serverFiles."prometheus.yml".scrape_configs[0].static_configs[0].targets[0] | string | `"localhost:9090"` | | 77 | | prometheus.serverFiles."prometheus.yml".scrape_configs[1].job_name | string | `"aws_quota_exporter"` | | 78 | | prometheus.serverFiles."prometheus.yml".scrape_configs[1].scrape_interval | string | `"15s"` | | 79 | | prometheus.serverFiles."prometheus.yml".scrape_configs[1].static_configs[0].targets[0] | string | `"aqe.default.svc.cluster.local:10100"` | | 80 | | readinessProbe.httpGet.path | string | `"/"` | | 81 | | readinessProbe.httpGet.port | string | `"http"` | | 82 | | readinessProbe.initialDelaySeconds | int | `60` | | 83 | | replicaCount | int | `1` | | 84 | | resources | object | `{}` | | 85 | | secret.AWS_ACCESS_KEY_ID | string | `""` | | 86 | | secret.AWS_SECRET_ACCESS_KEY | string | `""` | | 87 | | securityContext | object | `{}` | | 88 | | service.port | int | `10100` | | 89 | | service.type | string | `"ClusterIP"` | | 90 | | serviceAccount.annotations | object | `{}` | | 91 | | serviceAccount.create | bool | `false` | | 92 | | serviceAccount.name | string | `""` | | 93 | | serviceMonitor.additionalLabels | object | `{}` | | 94 | | serviceMonitor.create | bool | `false` | | 95 | | serviceMonitor.interval | string | `nil` | | 96 | | serviceMonitor.metricRelabelings | list | `[]` | | 97 | | serviceMonitor.namespace | string | `nil` | | 98 | | serviceMonitor.relabelings | list | `[]` | | 99 | | serviceMonitor.scrapeTimeout | string | `nil` | | 100 | | tolerations | list | `[]` | | 101 | 102 | ---------------------------------------------- 103 | Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) 104 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .vscode 20 | .project 21 | .idea/ 22 | *.tmproj 23 | OWNERS 24 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: grafana 3 | version: 8.10.1 4 | appVersion: 11.5.2 5 | kubeVersion: "^1.8.0-0" 6 | description: The leading tool for querying and visualizing time series and metrics. 7 | home: https://grafana.com 8 | icon: https://artifacthub.io/image/b4fed1a7-6c8f-4945-b99d-096efa3e4116 9 | sources: 10 | - https://github.com/grafana/grafana 11 | - https://github.com/grafana/helm-charts 12 | annotations: 13 | "artifacthub.io/license": Apache-2.0 14 | "artifacthub.io/links": | 15 | - name: Chart Source 16 | url: https://github.com/grafana/helm-charts 17 | - name: Upstream Project 18 | url: https://github.com/grafana/grafana 19 | maintainers: 20 | - name: zanhsieh 21 | email: zanhsieh@gmail.com 22 | - name: rtluckie 23 | email: rluckie@cisco.com 24 | - name: maorfr 25 | email: maor.friedman@redhat.com 26 | - name: Xtigyro 27 | email: miroslav.hadzhiev@gmail.com 28 | - name: torstenwalter 29 | email: mail@torstenwalter.de 30 | - name: jkroepke 31 | email: github@jkroepke.de 32 | type: application 33 | keywords: 34 | - monitoring 35 | - metric 36 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/ci/default-values.yaml: -------------------------------------------------------------------------------- 1 | # Leave this file empty to ensure that CI runs builds against the default configuration in values.yaml. 2 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/ci/with-affinity-values.yaml: -------------------------------------------------------------------------------- 1 | affinity: 2 | podAntiAffinity: 3 | preferredDuringSchedulingIgnoredDuringExecution: 4 | - podAffinityTerm: 5 | labelSelector: 6 | matchLabels: 7 | app.kubernetes.io/instance: grafana-test 8 | app.kubernetes.io/name: grafana 9 | topologyKey: failure-domain.beta.kubernetes.io/zone 10 | weight: 100 11 | requiredDuringSchedulingIgnoredDuringExecution: 12 | - labelSelector: 13 | matchLabels: 14 | app.kubernetes.io/instance: grafana-test 15 | app.kubernetes.io/name: grafana 16 | topologyKey: kubernetes.io/hostname 17 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/ci/with-dashboard-json-values.yaml: -------------------------------------------------------------------------------- 1 | dashboards: 2 | my-provider: 3 | my-awesome-dashboard: 4 | # An empty but valid dashboard 5 | json: | 6 | { 7 | "__inputs": [], 8 | "__requires": [ 9 | { 10 | "type": "grafana", 11 | "id": "grafana", 12 | "name": "Grafana", 13 | "version": "6.3.5" 14 | } 15 | ], 16 | "annotations": { 17 | "list": [ 18 | { 19 | "builtIn": 1, 20 | "datasource": "-- Grafana --", 21 | "enable": true, 22 | "hide": true, 23 | "iconColor": "rgba(0, 211, 255, 1)", 24 | "name": "Annotations & Alerts", 25 | "type": "dashboard" 26 | } 27 | ] 28 | }, 29 | "editable": true, 30 | "gnetId": null, 31 | "graphTooltip": 0, 32 | "id": null, 33 | "links": [], 34 | "panels": [], 35 | "schemaVersion": 19, 36 | "style": "dark", 37 | "tags": [], 38 | "templating": { 39 | "list": [] 40 | }, 41 | "time": { 42 | "from": "now-6h", 43 | "to": "now" 44 | }, 45 | "timepicker": { 46 | "refresh_intervals": ["5s"] 47 | }, 48 | "timezone": "", 49 | "title": "Dummy Dashboard", 50 | "uid": "IdcYQooWk", 51 | "version": 1 52 | } 53 | datasource: Prometheus 54 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/ci/with-dashboard-values.yaml: -------------------------------------------------------------------------------- 1 | dashboards: 2 | my-provider: 3 | my-awesome-dashboard: 4 | gnetId: 10000 5 | revision: 1 6 | datasource: Prometheus 7 | dashboardProviders: 8 | dashboardproviders.yaml: 9 | apiVersion: 1 10 | providers: 11 | - name: 'my-provider' 12 | orgId: 1 13 | folder: '' 14 | type: file 15 | updateIntervalSeconds: 10 16 | disableDeletion: true 17 | editable: true 18 | options: 19 | path: /var/lib/grafana/dashboards/my-provider 20 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/ci/with-extraconfigmapmounts-values.yaml: -------------------------------------------------------------------------------- 1 | extraConfigmapMounts: 2 | - name: '{{ include "grafana.fullname" . }}' 3 | configMap: '{{ include "grafana.fullname" . }}' 4 | mountPath: /var/lib/grafana/dashboards/test-dashboard.json 5 | # This is not a realistic test, but for this we only care about extraConfigmapMounts not being empty and pointing to an existing ConfigMap 6 | subPath: grafana.ini 7 | readOnly: true 8 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/ci/with-image-renderer-values.yaml: -------------------------------------------------------------------------------- 1 | podLabels: 2 | customLableA: Aaaaa 3 | imageRenderer: 4 | enabled: true 5 | env: 6 | RENDERING_ARGS: --disable-gpu,--window-size=1280x758 7 | RENDERING_MODE: clustered 8 | podLabels: 9 | customLableB: Bbbbb 10 | networkPolicy: 11 | limitIngress: true 12 | limitEgress: true 13 | resources: 14 | limits: 15 | cpu: 1000m 16 | memory: 1000Mi 17 | requests: 18 | cpu: 500m 19 | memory: 50Mi 20 | extraVolumes: 21 | - name: empty-renderer-volume 22 | emtpyDir: {} 23 | extraVolumeMounts: 24 | - mountPath: /tmp/renderer 25 | name: empty-renderer-volume 26 | extraConfigmapMounts: 27 | - name: renderer-config 28 | mountPath: /usr/src/app/config.json 29 | subPath: renderer-config.json 30 | configMap: image-renderer-config 31 | extraSecretMounts: 32 | - name: renderer-certificate 33 | mountPath: /usr/src/app/certs/ 34 | secretName: image-renderer-certificate 35 | readOnly: true 36 | 37 | extraObjects: 38 | - apiVersion: v1 39 | kind: ConfigMap 40 | metadata: 41 | name: image-renderer-config 42 | data: 43 | renderer-config.json: | 44 | { 45 | "service": { 46 | "host": null, 47 | "port": 8081, 48 | "protocol": "http", 49 | "certFile": "", 50 | "certKey": "", 51 | 52 | "metrics": { 53 | "enabled": true, 54 | "collectDefaultMetrics": true, 55 | "requestDurationBuckets": [1, 5, 7, 9, 11, 13, 15, 20, 30] 56 | }, 57 | 58 | "logging": { 59 | "level": "info", 60 | "console": { 61 | "json": true, 62 | "colorize": false 63 | } 64 | }, 65 | 66 | "security": { 67 | "authToken": "-" 68 | } 69 | }, 70 | "rendering": { 71 | "chromeBin": null, 72 | "args": ["--no-sandbox", "--disable-gpu"], 73 | "ignoresHttpsErrors": false, 74 | 75 | "timezone": null, 76 | "acceptLanguage": null, 77 | "width": 1000, 78 | "height": 500, 79 | "deviceScaleFactor": 1, 80 | "maxWidth": 3080, 81 | "maxHeight": 3000, 82 | "maxDeviceScaleFactor": 4, 83 | "pageZoomLevel": 1, 84 | "headed": false, 85 | 86 | "mode": "default", 87 | "emulateNetworkConditions": false, 88 | "clustering": { 89 | "monitor": false, 90 | "mode": "browser", 91 | "maxConcurrency": 5, 92 | "timeout": 30 93 | }, 94 | 95 | "verboseLogging": false, 96 | "dumpio": false, 97 | "timingMetrics": false 98 | } 99 | } 100 | - apiVersion: v1 101 | kind: Secret 102 | metadata: 103 | name: image-renderer-certificate 104 | type: Opaque 105 | data: 106 | # Decodes to 'PLACEHOLDER CERTIFICATE' 107 | not-a-real-certificate: UExBQ0VIT0xERVIgQ0VSVElGSUNBVEU= 108 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/ci/with-nondefault-values.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | environment: prod 3 | ingress: 4 | enabled: true 5 | hosts: 6 | - monitoring-{{ .Values.global.environment }}.example.com 7 | 8 | route: 9 | main: 10 | enabled: true 11 | labels: 12 | app: monitoring-prometheus 13 | hostnames: 14 | - "*.example.com" 15 | - "{{ .Values.global.environment }}.example.com" 16 | filters: 17 | - type: RequestHeaderModifier 18 | requestHeaderModifier: 19 | set: 20 | - name: my-header-name 21 | value: my-new-header-value 22 | additionalRules: 23 | - filters: 24 | - type: RequestHeaderModifier 25 | requestHeaderModifier: 26 | set: 27 | - name: my-header-name 28 | value: my-new-header-value 29 | matches: 30 | - path: 31 | type: PathPrefix 32 | value: /foo/ 33 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/ci/with-persistence.yaml: -------------------------------------------------------------------------------- 1 | persistence: 2 | type: pvc 3 | enabled: true 4 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/ci/with-sidecars-envvaluefrom-values.yaml: -------------------------------------------------------------------------------- 1 | extraObjects: 2 | - apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: '{{ include "grafana.fullname" . }}-test' 6 | data: 7 | var1: "value1" 8 | - apiVersion: v1 9 | kind: Secret 10 | metadata: 11 | name: '{{ include "grafana.fullname" . }}-test' 12 | type: Opaque 13 | data: 14 | var2: "dmFsdWUy" 15 | 16 | sidecar: 17 | dashboards: 18 | enabled: true 19 | envValueFrom: 20 | VAR1: 21 | configMapKeyRef: 22 | name: '{{ include "grafana.fullname" . }}-test' 23 | key: var1 24 | VAR2: 25 | secretKeyRef: 26 | name: '{{ include "grafana.fullname" . }}-test' 27 | key: var2 28 | datasources: 29 | enabled: true 30 | envValueFrom: 31 | VAR1: 32 | configMapKeyRef: 33 | name: '{{ include "grafana.fullname" . }}-test' 34 | key: var1 35 | VAR2: 36 | secretKeyRef: 37 | name: '{{ include "grafana.fullname" . }}-test' 38 | key: var2 39 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get your '{{ .Values.adminUser }}' user password by running: 2 | 3 | kubectl get secret --namespace {{ include "grafana.namespace" . }} {{ .Values.admin.existingSecret | default (include "grafana.fullname" .) }} -o jsonpath="{.data.{{ .Values.admin.passwordKey | default "admin-password" }}}" | base64 --decode ; echo 4 | 5 | 6 | 2. The Grafana server can be accessed via port {{ .Values.service.port }} on the following DNS name from within your cluster: 7 | 8 | {{ include "grafana.fullname" . }}.{{ include "grafana.namespace" . }}.svc.cluster.local 9 | {{ if .Values.ingress.enabled }} 10 | If you bind grafana to 80, please update values in values.yaml and reinstall: 11 | ``` 12 | securityContext: 13 | runAsUser: 0 14 | runAsGroup: 0 15 | fsGroup: 0 16 | 17 | command: 18 | - "setcap" 19 | - "'cap_net_bind_service=+ep'" 20 | - "/usr/sbin/grafana-server &&" 21 | - "sh" 22 | - "/run.sh" 23 | ``` 24 | Details refer to https://grafana.com/docs/installation/configuration/#http-port. 25 | Or grafana would always crash. 26 | 27 | From outside the cluster, the server URL(s) are: 28 | {{- range .Values.ingress.hosts }} 29 | http://{{ . }} 30 | {{- end }} 31 | {{- else }} 32 | Get the Grafana URL to visit by running these commands in the same shell: 33 | {{- if contains "NodePort" .Values.service.type }} 34 | export NODE_PORT=$(kubectl get --namespace {{ include "grafana.namespace" . }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "grafana.fullname" . }}) 35 | export NODE_IP=$(kubectl get nodes --namespace {{ include "grafana.namespace" . }} -o jsonpath="{.items[0].status.addresses[0].address}") 36 | echo http://$NODE_IP:$NODE_PORT 37 | {{- else if contains "LoadBalancer" .Values.service.type }} 38 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 39 | You can watch the status of by running 'kubectl get svc --namespace {{ include "grafana.namespace" . }} -w {{ include "grafana.fullname" . }}' 40 | export SERVICE_IP=$(kubectl get svc --namespace {{ include "grafana.namespace" . }} {{ include "grafana.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 41 | http://$SERVICE_IP:{{ .Values.service.port -}} 42 | {{- else if contains "ClusterIP" .Values.service.type }} 43 | export POD_NAME=$(kubectl get pods --namespace {{ include "grafana.namespace" . }} -l "app.kubernetes.io/name={{ include "grafana.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 44 | kubectl --namespace {{ include "grafana.namespace" . }} port-forward $POD_NAME 3000 45 | {{- end }} 46 | {{- end }} 47 | 48 | 3. Login with the password from step 1 and the username: {{ .Values.adminUser }} 49 | 50 | {{- if and (not .Values.persistence.enabled) (not .Values.persistence.disableWarning) }} 51 | ################################################################################# 52 | ###### WARNING: Persistence is disabled!!! You will lose your data when ##### 53 | ###### the Grafana pod is terminated. ##### 54 | ################################################################################# 55 | {{- end }} 56 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/_config.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Generate config map data 3 | */}} 4 | {{- define "grafana.configData" -}} 5 | {{ include "grafana.assertNoLeakedSecrets" . }} 6 | {{- $files := .Files }} 7 | {{- $root := . -}} 8 | {{- with .Values.plugins }} 9 | plugins: {{ join "," . }} 10 | {{- end }} 11 | grafana.ini: | 12 | {{- range $elem, $elemVal := index .Values "grafana.ini" }} 13 | {{- if not (kindIs "map" $elemVal) }} 14 | {{- if kindIs "invalid" $elemVal }} 15 | {{ $elem }} = 16 | {{- else if kindIs "slice" $elemVal }} 17 | {{ $elem }} = {{ toJson $elemVal }} 18 | {{- else if kindIs "string" $elemVal }} 19 | {{ $elem }} = {{ tpl $elemVal $ }} 20 | {{- else }} 21 | {{ $elem }} = {{ $elemVal }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | {{- range $key, $value := index .Values "grafana.ini" }} 26 | {{- if kindIs "map" $value }} 27 | [{{ $key }}] 28 | {{- range $elem, $elemVal := $value }} 29 | {{- if kindIs "invalid" $elemVal }} 30 | {{ $elem }} = 31 | {{- else if kindIs "slice" $elemVal }} 32 | {{ $elem }} = {{ toJson $elemVal }} 33 | {{- else if kindIs "string" $elemVal }} 34 | {{ $elem }} = {{ tpl $elemVal $ }} 35 | {{- else }} 36 | {{ $elem }} = {{ $elemVal }} 37 | {{- end }} 38 | {{- end }} 39 | {{- end }} 40 | {{- end }} 41 | 42 | {{- range $key, $value := .Values.datasources }} 43 | {{- if not (hasKey $value "secret") }} 44 | {{ $key }}: | 45 | {{- tpl (toYaml $value | nindent 2) $root }} 46 | {{- end }} 47 | {{- end }} 48 | 49 | {{- range $key, $value := .Values.notifiers }} 50 | {{- if not (hasKey $value "secret") }} 51 | {{ $key }}: | 52 | {{- toYaml $value | nindent 2 }} 53 | {{- end }} 54 | {{- end }} 55 | 56 | {{- range $key, $value := .Values.alerting }} 57 | {{- if (hasKey $value "file") }} 58 | {{ $key }}: 59 | {{- toYaml ( $files.Get $value.file ) | nindent 2 }} 60 | {{- else if (or (hasKey $value "secret") (hasKey $value "secretFile"))}} 61 | {{/* will be stored inside secret generated by "configSecret.yaml"*/}} 62 | {{- else }} 63 | {{ $key }}: | 64 | {{- tpl (toYaml $value | nindent 2) $root }} 65 | {{- end }} 66 | {{- end }} 67 | 68 | {{- range $key, $value := .Values.dashboardProviders }} 69 | {{ $key }}: | 70 | {{- toYaml $value | nindent 2 }} 71 | {{- end }} 72 | 73 | {{- if .Values.dashboards }} 74 | download_dashboards.sh: | 75 | #!/usr/bin/env sh 76 | set -euf 77 | {{- if .Values.dashboardProviders }} 78 | {{- range $key, $value := .Values.dashboardProviders }} 79 | {{- range $value.providers }} 80 | mkdir -p {{ .options.path }} 81 | {{- end }} 82 | {{- end }} 83 | {{- end }} 84 | {{ $dashboardProviders := .Values.dashboardProviders }} 85 | {{- range $provider, $dashboards := .Values.dashboards }} 86 | {{- range $key, $value := $dashboards }} 87 | {{- if (or (hasKey $value "gnetId") (hasKey $value "url")) }} 88 | curl -skf \ 89 | --connect-timeout 60 \ 90 | --max-time 60 \ 91 | {{- if not $value.b64content }} 92 | {{- if not $value.acceptHeader }} 93 | -H "Accept: application/json" \ 94 | {{- else }} 95 | -H "Accept: {{ $value.acceptHeader }}" \ 96 | {{- end }} 97 | {{- if $value.token }} 98 | -H "Authorization: token {{ $value.token }}" \ 99 | {{- end }} 100 | {{- if $value.bearerToken }} 101 | -H "Authorization: Bearer {{ $value.bearerToken }}" \ 102 | {{- end }} 103 | {{- if $value.basic }} 104 | -H "Authorization: Basic {{ $value.basic }}" \ 105 | {{- end }} 106 | {{- if $value.gitlabToken }} 107 | -H "PRIVATE-TOKEN: {{ $value.gitlabToken }}" \ 108 | {{- end }} 109 | -H "Content-Type: application/json;charset=UTF-8" \ 110 | {{- end }} 111 | {{- $dpPath := "" -}} 112 | {{- range $kd := (index $dashboardProviders "dashboardproviders.yaml").providers }} 113 | {{- if eq $kd.name $provider }} 114 | {{- $dpPath = $kd.options.path }} 115 | {{- end }} 116 | {{- end }} 117 | {{- if $value.url }} 118 | "{{ $value.url }}" \ 119 | {{- else }} 120 | "https://grafana.com/api/dashboards/{{ $value.gnetId }}/revisions/{{- if $value.revision -}}{{ $value.revision }}{{- else -}}1{{- end -}}/download" \ 121 | {{- end }} 122 | {{- if $value.datasource }} 123 | {{- if kindIs "string" $value.datasource }} 124 | | sed '/-- .* --/! s/"datasource":.*,/"datasource": "{{ $value.datasource }}",/g' \ 125 | {{- end }} 126 | {{- if kindIs "slice" $value.datasource }} 127 | {{- range $value.datasource }} 128 | | sed '/-- .* --/! s/${{"{"}}{{ .name }}}/{{ .value }}/g' \ 129 | {{- end }} 130 | {{- end }} 131 | {{- end }} 132 | {{- if $value.b64content }} 133 | | base64 -d \ 134 | {{- end }} 135 | > "{{- if $dpPath -}}{{ $dpPath }}{{- else -}}/var/lib/grafana/dashboards/{{ $provider }}{{- end -}}/{{ $key }}.json" 136 | {{ end }} 137 | {{- end }} 138 | {{- end }} 139 | {{- end }} 140 | {{- end -}} 141 | 142 | {{/* 143 | Generate dashboard json config map data 144 | */}} 145 | {{- define "grafana.configDashboardProviderData" -}} 146 | provider.yaml: |- 147 | apiVersion: 1 148 | providers: 149 | - name: '{{ .Values.sidecar.dashboards.provider.name }}' 150 | orgId: {{ .Values.sidecar.dashboards.provider.orgid }} 151 | {{- if not .Values.sidecar.dashboards.provider.foldersFromFilesStructure }} 152 | folder: '{{ .Values.sidecar.dashboards.provider.folder }}' 153 | folderUid: '{{ .Values.sidecar.dashboards.provider.folderUid }}' 154 | {{- end }} 155 | type: {{ .Values.sidecar.dashboards.provider.type }} 156 | disableDeletion: {{ .Values.sidecar.dashboards.provider.disableDelete }} 157 | allowUiUpdates: {{ .Values.sidecar.dashboards.provider.allowUiUpdates }} 158 | updateIntervalSeconds: {{ .Values.sidecar.dashboards.provider.updateIntervalSeconds | default 30 }} 159 | options: 160 | foldersFromFilesStructure: {{ .Values.sidecar.dashboards.provider.foldersFromFilesStructure }} 161 | path: {{ .Values.sidecar.dashboards.folder }}{{- with .Values.sidecar.dashboards.defaultFolderName }}/{{ . }}{{- end }} 162 | {{- end -}} 163 | 164 | {{- define "grafana.secretsData" -}} 165 | {{- if and (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) (not .Values.admin.existingSecret) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD) }} 166 | admin-user: {{ .Values.adminUser | b64enc | quote }} 167 | {{- if .Values.adminPassword }} 168 | admin-password: {{ .Values.adminPassword | b64enc | quote }} 169 | {{- else }} 170 | admin-password: {{ include "grafana.password" . }} 171 | {{- end }} 172 | {{- end }} 173 | {{- if not .Values.ldap.existingSecret }} 174 | ldap-toml: {{ tpl .Values.ldap.config $ | b64enc | quote }} 175 | {{- end }} 176 | {{- end -}} 177 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "grafana.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 7 | {{- end }} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "grafana.fullname" -}} 15 | {{- if .Values.fullnameOverride }} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 17 | {{- else }} 18 | {{- $name := default .Chart.Name .Values.nameOverride }} 19 | {{- if contains $name .Release.Name }} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 21 | {{- else }} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 23 | {{- end }} 24 | {{- end }} 25 | {{- end }} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "grafana.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 32 | {{- end }} 33 | 34 | {{/* 35 | Create the name of the service account 36 | */}} 37 | {{- define "grafana.serviceAccountName" -}} 38 | {{- if .Values.serviceAccount.create }} 39 | {{- default (include "grafana.fullname" .) .Values.serviceAccount.name }} 40 | {{- else }} 41 | {{- default "default" .Values.serviceAccount.name }} 42 | {{- end }} 43 | {{- end }} 44 | 45 | {{- define "grafana.serviceAccountNameTest" -}} 46 | {{- if .Values.serviceAccount.create }} 47 | {{- default (print (include "grafana.fullname" .) "-test") .Values.serviceAccount.nameTest }} 48 | {{- else }} 49 | {{- default "default" .Values.serviceAccount.nameTest }} 50 | {{- end }} 51 | {{- end }} 52 | 53 | {{/* 54 | Allow the release namespace to be overridden for multi-namespace deployments in combined charts 55 | */}} 56 | {{- define "grafana.namespace" -}} 57 | {{- if .Values.namespaceOverride }} 58 | {{- .Values.namespaceOverride }} 59 | {{- else }} 60 | {{- .Release.Namespace }} 61 | {{- end }} 62 | {{- end }} 63 | 64 | {{/* 65 | Common labels 66 | */}} 67 | {{- define "grafana.labels" -}} 68 | helm.sh/chart: {{ include "grafana.chart" . }} 69 | {{ include "grafana.selectorLabels" . }} 70 | {{- if or .Chart.AppVersion .Values.image.tag }} 71 | app.kubernetes.io/version: {{ mustRegexReplaceAllLiteral "@sha.*" .Values.image.tag "" | default .Chart.AppVersion | trunc 63 | trimSuffix "-" | quote }} 72 | {{- end }} 73 | {{- with .Values.extraLabels }} 74 | {{ toYaml . }} 75 | {{- end }} 76 | {{- end }} 77 | 78 | {{/* 79 | Selector labels 80 | */}} 81 | {{- define "grafana.selectorLabels" -}} 82 | app.kubernetes.io/name: {{ include "grafana.name" . }} 83 | app.kubernetes.io/instance: {{ .Release.Name }} 84 | {{- end }} 85 | 86 | {{/* 87 | Common labels 88 | */}} 89 | {{- define "grafana.imageRenderer.labels" -}} 90 | helm.sh/chart: {{ include "grafana.chart" . }} 91 | {{ include "grafana.imageRenderer.selectorLabels" . }} 92 | {{- if or .Chart.AppVersion .Values.image.tag }} 93 | app.kubernetes.io/version: {{ mustRegexReplaceAllLiteral "@sha.*" .Values.image.tag "" | default .Chart.AppVersion | trunc 63 | trimSuffix "-" | quote }} 94 | {{- end }} 95 | {{- end }} 96 | 97 | {{/* 98 | Selector labels ImageRenderer 99 | */}} 100 | {{- define "grafana.imageRenderer.selectorLabels" -}} 101 | app.kubernetes.io/name: {{ include "grafana.name" . }}-image-renderer 102 | app.kubernetes.io/instance: {{ .Release.Name }} 103 | {{- end }} 104 | 105 | {{/* 106 | Looks if there's an existing secret and reuse its password. If not it generates 107 | new password and use it. 108 | */}} 109 | {{- define "grafana.password" -}} 110 | {{- $secret := (lookup "v1" "Secret" (include "grafana.namespace" .) (include "grafana.fullname" .) ) }} 111 | {{- if $secret }} 112 | {{- index $secret "data" "admin-password" }} 113 | {{- else }} 114 | {{- (randAlphaNum 40) | b64enc | quote }} 115 | {{- end }} 116 | {{- end }} 117 | 118 | {{/* 119 | Return the appropriate apiVersion for rbac. 120 | */}} 121 | {{- define "grafana.rbac.apiVersion" -}} 122 | {{- if $.Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1" }} 123 | {{- print "rbac.authorization.k8s.io/v1" }} 124 | {{- else }} 125 | {{- print "rbac.authorization.k8s.io/v1beta1" }} 126 | {{- end }} 127 | {{- end }} 128 | 129 | {{/* 130 | Return the appropriate apiVersion for ingress. 131 | */}} 132 | {{- define "grafana.ingress.apiVersion" -}} 133 | {{- if and ($.Capabilities.APIVersions.Has "networking.k8s.io/v1") (semverCompare ">= 1.19-0" .Capabilities.KubeVersion.Version) }} 134 | {{- print "networking.k8s.io/v1" }} 135 | {{- else if $.Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" }} 136 | {{- print "networking.k8s.io/v1beta1" }} 137 | {{- else }} 138 | {{- print "extensions/v1beta1" }} 139 | {{- end }} 140 | {{- end }} 141 | 142 | {{/* 143 | Return the appropriate apiVersion for Horizontal Pod Autoscaler. 144 | */}} 145 | {{- define "grafana.hpa.apiVersion" -}} 146 | {{- if .Capabilities.APIVersions.Has "autoscaling/v2" }} 147 | {{- print "autoscaling/v2" }} 148 | {{- else }} 149 | {{- print "autoscaling/v2beta2" }} 150 | {{- end }} 151 | {{- end }} 152 | 153 | {{/* 154 | Return the appropriate apiVersion for podDisruptionBudget. 155 | */}} 156 | {{- define "grafana.podDisruptionBudget.apiVersion" -}} 157 | {{- if $.Values.podDisruptionBudget.apiVersion }} 158 | {{- print $.Values.podDisruptionBudget.apiVersion }} 159 | {{- else if $.Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }} 160 | {{- print "policy/v1" }} 161 | {{- else }} 162 | {{- print "policy/v1beta1" }} 163 | {{- end }} 164 | {{- end }} 165 | 166 | {{/* 167 | Return if ingress is stable. 168 | */}} 169 | {{- define "grafana.ingress.isStable" -}} 170 | {{- eq (include "grafana.ingress.apiVersion" .) "networking.k8s.io/v1" }} 171 | {{- end }} 172 | 173 | {{/* 174 | Return if ingress supports ingressClassName. 175 | */}} 176 | {{- define "grafana.ingress.supportsIngressClassName" -}} 177 | {{- or (eq (include "grafana.ingress.isStable" .) "true") (and (eq (include "grafana.ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" .Capabilities.KubeVersion.Version)) }} 178 | {{- end }} 179 | 180 | {{/* 181 | Return if ingress supports pathType. 182 | */}} 183 | {{- define "grafana.ingress.supportsPathType" -}} 184 | {{- or (eq (include "grafana.ingress.isStable" .) "true") (and (eq (include "grafana.ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" .Capabilities.KubeVersion.Version)) }} 185 | {{- end }} 186 | 187 | {{/* 188 | Formats imagePullSecrets. Input is (dict "root" . "imagePullSecrets" .{specific imagePullSecrets}) 189 | */}} 190 | {{- define "grafana.imagePullSecrets" -}} 191 | {{- $root := .root }} 192 | {{- range (concat .root.Values.global.imagePullSecrets .imagePullSecrets) }} 193 | {{- if eq (typeOf .) "map[string]interface {}" }} 194 | - {{ toYaml (dict "name" (tpl .name $root)) | trim }} 195 | {{- else }} 196 | - name: {{ tpl . $root }} 197 | {{- end }} 198 | {{- end }} 199 | {{- end }} 200 | 201 | 202 | {{/* 203 | Checks whether or not the configSecret secret has to be created 204 | */}} 205 | {{- define "grafana.shouldCreateConfigSecret" -}} 206 | {{- $secretFound := false -}} 207 | {{- range $key, $value := .Values.datasources }} 208 | {{- if hasKey $value "secret" }} 209 | {{- $secretFound = true}} 210 | {{- end }} 211 | {{- end }} 212 | {{- range $key, $value := .Values.notifiers }} 213 | {{- if hasKey $value "secret" }} 214 | {{- $secretFound = true}} 215 | {{- end }} 216 | {{- end }} 217 | {{- range $key, $value := .Values.alerting }} 218 | {{- if (or (hasKey $value "secret") (hasKey $value "secretFile")) }} 219 | {{- $secretFound = true}} 220 | {{- end }} 221 | {{- end }} 222 | {{- $secretFound}} 223 | {{- end -}} 224 | 225 | {{/* 226 | Checks whether the user is attempting to store secrets in plaintext 227 | in the grafana.ini configmap 228 | */}} 229 | {{/* grafana.assertNoLeakedSecrets checks for sensitive keys in values */}} 230 | {{- define "grafana.assertNoLeakedSecrets" -}} 231 | {{- $sensitiveKeysYaml := ` 232 | sensitiveKeys: 233 | - path: ["database", "password"] 234 | - path: ["smtp", "password"] 235 | - path: ["security", "secret_key"] 236 | - path: ["security", "admin_password"] 237 | - path: ["auth.basic", "password"] 238 | - path: ["auth.ldap", "bind_password"] 239 | - path: ["auth.google", "client_secret"] 240 | - path: ["auth.github", "client_secret"] 241 | - path: ["auth.gitlab", "client_secret"] 242 | - path: ["auth.generic_oauth", "client_secret"] 243 | - path: ["auth.okta", "client_secret"] 244 | - path: ["auth.azuread", "client_secret"] 245 | - path: ["auth.grafana_com", "client_secret"] 246 | - path: ["auth.grafananet", "client_secret"] 247 | - path: ["azure", "user_identity_client_secret"] 248 | - path: ["unified_alerting", "ha_redis_password"] 249 | - path: ["metrics", "basic_auth_password"] 250 | - path: ["external_image_storage.s3", "secret_key"] 251 | - path: ["external_image_storage.webdav", "password"] 252 | - path: ["external_image_storage.azure_blob", "account_key"] 253 | ` | fromYaml -}} 254 | {{- if $.Values.assertNoLeakedSecrets -}} 255 | {{- $grafanaIni := index .Values "grafana.ini" -}} 256 | {{- range $_, $secret := $sensitiveKeysYaml.sensitiveKeys -}} 257 | {{- $currentMap := $grafanaIni -}} 258 | {{- $shouldContinue := true -}} 259 | {{- range $index, $elem := $secret.path -}} 260 | {{- if and $shouldContinue (hasKey $currentMap $elem) -}} 261 | {{- if eq (len $secret.path) (add1 $index) -}} 262 | {{- if not (regexMatch "\\$(?:__(?:env|file|vault))?{[^}]+}" (index $currentMap $elem)) -}} 263 | {{- fail (printf "Sensitive key '%s' should not be defined explicitly in values. Use variable expansion instead. You can disable this client-side validation by changing the value of assertNoLeakedSecrets." (join "." $secret.path)) -}} 264 | {{- end -}} 265 | {{- else -}} 266 | {{- $currentMap = index $currentMap $elem -}} 267 | {{- end -}} 268 | {{- else -}} 269 | {{- $shouldContinue = false -}} 270 | {{- end -}} 271 | {{- end -}} 272 | {{- end -}} 273 | {{- end -}} 274 | {{- end -}} 275 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create (or (not .Values.rbac.namespaced) .Values.rbac.extraClusterRoleRules) (not .Values.rbac.useExistingClusterRole) }} 2 | kind: ClusterRole 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | labels: 6 | {{- include "grafana.labels" . | nindent 4 }} 7 | {{- with .Values.annotations }} 8 | annotations: 9 | {{- toYaml . | nindent 4 }} 10 | {{- end }} 11 | name: {{ include "grafana.fullname" . }}-clusterrole 12 | {{- if or .Values.sidecar.dashboards.enabled .Values.rbac.extraClusterRoleRules .Values.sidecar.datasources.enabled .Values.sidecar.plugins.enabled .Values.sidecar.alerts.enabled }} 13 | rules: 14 | {{- if or .Values.sidecar.dashboards.enabled .Values.sidecar.datasources.enabled .Values.sidecar.plugins.enabled .Values.sidecar.alerts.enabled }} 15 | - apiGroups: [""] # "" indicates the core API group 16 | resources: ["configmaps", "secrets"] 17 | verbs: ["get", "watch", "list"] 18 | {{- end}} 19 | {{- with .Values.rbac.extraClusterRoleRules }} 20 | {{- toYaml . | nindent 2 }} 21 | {{- end}} 22 | {{- else }} 23 | rules: [] 24 | {{- end}} 25 | {{- end}} 26 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create (or (not .Values.rbac.namespaced) .Values.rbac.extraClusterRoleRules) }} 2 | kind: ClusterRoleBinding 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: {{ include "grafana.fullname" . }}-clusterrolebinding 6 | labels: 7 | {{- include "grafana.labels" . | nindent 4 }} 8 | {{- with .Values.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "grafana.serviceAccountName" . }} 15 | namespace: {{ include "grafana.namespace" . }} 16 | roleRef: 17 | kind: ClusterRole 18 | {{- if .Values.rbac.useExistingClusterRole }} 19 | name: {{ .Values.rbac.useExistingClusterRole }} 20 | {{- else }} 21 | name: {{ include "grafana.fullname" . }}-clusterrole 22 | {{- end }} 23 | apiGroup: rbac.authorization.k8s.io 24 | {{- end }} 25 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/configSecret.yaml: -------------------------------------------------------------------------------- 1 | {{- $createConfigSecret := eq (include "grafana.shouldCreateConfigSecret" .) "true" -}} 2 | {{- if and .Values.createConfigmap $createConfigSecret }} 3 | {{- $files := .Files }} 4 | {{- $root := . -}} 5 | apiVersion: v1 6 | kind: Secret 7 | metadata: 8 | name: "{{ include "grafana.fullname" . }}-config-secret" 9 | namespace: {{ include "grafana.namespace" . }} 10 | labels: 11 | {{- include "grafana.labels" . | nindent 4 }} 12 | {{- with .Values.annotations }} 13 | annotations: 14 | {{- toYaml . | nindent 4 }} 15 | {{- end }} 16 | data: 17 | {{- range $key, $value := .Values.alerting }} 18 | {{- if (hasKey $value "secretFile") }} 19 | {{- $key | nindent 2 }}: 20 | {{- toYaml ( $files.Get $value.secretFile ) | b64enc | nindent 4}} 21 | {{/* as of https://helm.sh/docs/chart_template_guide/accessing_files/ this will only work if you fork this chart and add files to it*/}} 22 | {{- end }} 23 | {{- end }} 24 | stringData: 25 | {{- range $key, $value := .Values.datasources }} 26 | {{- if (hasKey $value "secret") }} 27 | {{- $key | nindent 2 }}: | 28 | {{- tpl (toYaml $value.secret | nindent 4) $root }} 29 | {{- end }} 30 | {{- end }} 31 | {{- range $key, $value := .Values.notifiers }} 32 | {{- if (hasKey $value "secret") }} 33 | {{- $key | nindent 2 }}: | 34 | {{- tpl (toYaml $value.secret | nindent 4) $root }} 35 | {{- end }} 36 | {{- end }} 37 | {{- range $key, $value := .Values.alerting }} 38 | {{ if (hasKey $value "secret") }} 39 | {{- $key | nindent 2 }}: | 40 | {{- tpl (toYaml $value.secret | nindent 4) $root }} 41 | {{- end }} 42 | {{- end }} 43 | {{- end }} 44 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/configmap-dashboard-provider.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.sidecar.dashboards.enabled .Values.sidecar.dashboards.SCProvider }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | labels: 6 | {{- include "grafana.labels" . | nindent 4 }} 7 | {{- with .Values.annotations }} 8 | annotations: 9 | {{- toYaml . | nindent 4 }} 10 | {{- end }} 11 | name: {{ include "grafana.fullname" . }}-config-dashboards 12 | namespace: {{ include "grafana.namespace" . }} 13 | data: 14 | {{- include "grafana.configDashboardProviderData" . | nindent 2 }} 15 | {{- end }} 16 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.createConfigmap }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ include "grafana.fullname" . }} 6 | namespace: {{ include "grafana.namespace" . }} 7 | labels: 8 | {{- include "grafana.labels" . | nindent 4 }} 9 | {{- if or .Values.configMapAnnotations .Values.annotations }} 10 | annotations: 11 | {{- with .Values.annotations }} 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | {{- with .Values.configMapAnnotations }} 15 | {{- toYaml . | nindent 4 }} 16 | {{- end }} 17 | {{- end }} 18 | data: 19 | {{- include "grafana.configData" . | nindent 2 }} 20 | {{- end }} 21 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/dashboards-json-configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.dashboards }} 2 | {{ $files := .Files }} 3 | {{- range $provider, $dashboards := .Values.dashboards }} 4 | apiVersion: v1 5 | kind: ConfigMap 6 | metadata: 7 | name: {{ include "grafana.fullname" $ }}-dashboards-{{ $provider }} 8 | namespace: {{ include "grafana.namespace" $ }} 9 | labels: 10 | {{- include "grafana.labels" $ | nindent 4 }} 11 | dashboard-provider: {{ $provider }} 12 | {{- if $dashboards }} 13 | data: 14 | {{- $dashboardFound := false }} 15 | {{- range $key, $value := $dashboards }} 16 | {{- if (or (hasKey $value "json") (hasKey $value "file")) }} 17 | {{- $dashboardFound = true }} 18 | {{- print $key | nindent 2 }}.json: 19 | {{- if hasKey $value "json" }} 20 | |- 21 | {{- $value.json | nindent 6 }} 22 | {{- end }} 23 | {{- if hasKey $value "file" }} 24 | {{- toYaml ( $files.Get $value.file ) | nindent 4}} 25 | {{- end }} 26 | {{- end }} 27 | {{- end }} 28 | {{- if not $dashboardFound }} 29 | {} 30 | {{- end }} 31 | {{- end }} 32 | --- 33 | {{- end }} 34 | 35 | {{- end }} 36 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- if (and (not .Values.useStatefulSet) (or (not .Values.persistence.enabled) (eq .Values.persistence.type "pvc"))) }} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ include "grafana.fullname" . }} 6 | namespace: {{ include "grafana.namespace" . }} 7 | labels: 8 | {{- include "grafana.labels" . | nindent 4 }} 9 | {{- with .Values.labels }} 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- with .Values.annotations }} 13 | annotations: 14 | {{- toYaml . | nindent 4 }} 15 | {{- end }} 16 | spec: 17 | {{- if (not .Values.autoscaling.enabled) }} 18 | replicas: {{ .Values.replicas }} 19 | {{- end }} 20 | revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} 21 | selector: 22 | matchLabels: 23 | {{- include "grafana.selectorLabels" . | nindent 6 }} 24 | {{- with .Values.deploymentStrategy }} 25 | strategy: 26 | {{- toYaml . | trim | nindent 4 }} 27 | {{- end }} 28 | template: 29 | metadata: 30 | labels: 31 | {{- include "grafana.labels" . | nindent 8 }} 32 | {{- with .Values.podLabels }} 33 | {{- toYaml . | nindent 8 }} 34 | {{- end }} 35 | annotations: 36 | checksum/config: {{ include "grafana.configData" . | sha256sum }} 37 | {{- if .Values.dashboards }} 38 | checksum/dashboards-json-config: {{ include (print $.Template.BasePath "/dashboards-json-configmap.yaml") . | sha256sum }} 39 | {{- end }} 40 | checksum/sc-dashboard-provider-config: {{ include "grafana.configDashboardProviderData" . | sha256sum }} 41 | {{- if and (or (and (not .Values.admin.existingSecret) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD)) (and .Values.ldap.enabled (not .Values.ldap.existingSecret))) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} 42 | checksum/secret: {{ include "grafana.secretsData" . | sha256sum }} 43 | {{- end }} 44 | {{- if .Values.envRenderSecret }} 45 | checksum/secret-env: {{ tpl (toYaml .Values.envRenderSecret) . | sha256sum }} 46 | {{- end }} 47 | kubectl.kubernetes.io/default-container: {{ .Chart.Name }} 48 | {{- with .Values.podAnnotations }} 49 | {{- toYaml . | nindent 8 }} 50 | {{- end }} 51 | spec: 52 | {{- include "grafana.pod" . | nindent 6 }} 53 | {{- end }} 54 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/extra-manifests.yaml: -------------------------------------------------------------------------------- 1 | {{ range .Values.extraObjects }} 2 | --- 3 | {{ tpl (toYaml .) $ }} 4 | {{ end }} 5 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/headless-service.yaml: -------------------------------------------------------------------------------- 1 | {{- $sts := list "sts" "StatefulSet" "statefulset" -}} 2 | {{- if or .Values.headlessService (and .Values.persistence.enabled (not .Values.persistence.existingClaim) (has .Values.persistence.type $sts)) }} 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: {{ include "grafana.fullname" . }}-headless 7 | namespace: {{ include "grafana.namespace" . }} 8 | labels: 9 | {{- include "grafana.labels" . | nindent 4 }} 10 | {{- with .Values.annotations }} 11 | annotations: 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | spec: 15 | clusterIP: None 16 | selector: 17 | {{- include "grafana.selectorLabels" . | nindent 4 }} 18 | type: ClusterIP 19 | ports: 20 | - name: {{ .Values.gossipPortName }}-tcp 21 | port: 9094 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- $sts := list "sts" "StatefulSet" "statefulset" -}} 2 | {{- if .Values.autoscaling.enabled }} 3 | apiVersion: {{ include "grafana.hpa.apiVersion" . }} 4 | kind: HorizontalPodAutoscaler 5 | metadata: 6 | name: {{ include "grafana.fullname" . }} 7 | namespace: {{ include "grafana.namespace" . }} 8 | labels: 9 | app.kubernetes.io/name: {{ include "grafana.name" . }} 10 | helm.sh/chart: {{ include "grafana.chart" . }} 11 | app.kubernetes.io/instance: {{ .Release.Name }} 12 | spec: 13 | scaleTargetRef: 14 | apiVersion: apps/v1 15 | {{- if has .Values.persistence.type $sts }} 16 | kind: StatefulSet 17 | {{- else }} 18 | kind: Deployment 19 | {{- end }} 20 | name: {{ include "grafana.fullname" . }} 21 | minReplicas: {{ .Values.autoscaling.minReplicas }} 22 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 23 | metrics: 24 | {{- if .Values.autoscaling.targetMemory }} 25 | - type: Resource 26 | resource: 27 | name: memory 28 | {{- if eq (include "grafana.hpa.apiVersion" .) "autoscaling/v2beta1" }} 29 | targetAverageUtilization: {{ .Values.autoscaling.targetMemory }} 30 | {{- else }} 31 | target: 32 | type: Utilization 33 | averageUtilization: {{ .Values.autoscaling.targetMemory }} 34 | {{- end }} 35 | {{- end }} 36 | {{- if .Values.autoscaling.targetCPU }} 37 | - type: Resource 38 | resource: 39 | name: cpu 40 | {{- if eq (include "grafana.hpa.apiVersion" .) "autoscaling/v2beta1" }} 41 | targetAverageUtilization: {{ .Values.autoscaling.targetCPU }} 42 | {{- else }} 43 | target: 44 | type: Utilization 45 | averageUtilization: {{ .Values.autoscaling.targetCPU }} 46 | {{- end }} 47 | {{- end }} 48 | {{- if .Values.autoscaling.behavior }} 49 | behavior: {{ toYaml .Values.autoscaling.behavior | nindent 4 }} 50 | {{- end }} 51 | {{- end }} 52 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/image-renderer-deployment.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.imageRenderer.enabled }} 2 | {{- $root := . -}} 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: {{ include "grafana.fullname" . }}-image-renderer 7 | namespace: {{ include "grafana.namespace" . }} 8 | labels: 9 | {{- include "grafana.imageRenderer.labels" . | nindent 4 }} 10 | {{- with .Values.imageRenderer.labels }} 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | {{- with .Values.imageRenderer.annotations }} 14 | annotations: 15 | {{- toYaml . | nindent 4 }} 16 | {{- end }} 17 | spec: 18 | {{- if and (not .Values.imageRenderer.autoscaling.enabled) (.Values.imageRenderer.replicas) }} 19 | replicas: {{ .Values.imageRenderer.replicas }} 20 | {{- end }} 21 | revisionHistoryLimit: {{ .Values.imageRenderer.revisionHistoryLimit }} 22 | selector: 23 | matchLabels: 24 | {{- include "grafana.imageRenderer.selectorLabels" . | nindent 6 }} 25 | 26 | {{- with .Values.imageRenderer.deploymentStrategy }} 27 | strategy: 28 | {{- toYaml . | trim | nindent 4 }} 29 | {{- end }} 30 | template: 31 | metadata: 32 | labels: 33 | {{- include "grafana.imageRenderer.selectorLabels" . | nindent 8 }} 34 | {{- with .Values.imageRenderer.podLabels }} 35 | {{- toYaml . | nindent 8 }} 36 | {{- end }} 37 | annotations: 38 | checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} 39 | {{- with .Values.imageRenderer.podAnnotations }} 40 | {{- toYaml . | nindent 8 }} 41 | {{- end }} 42 | spec: 43 | {{- with .Values.imageRenderer.schedulerName }} 44 | schedulerName: "{{ . }}" 45 | {{- end }} 46 | {{- with .Values.imageRenderer.serviceAccountName }} 47 | serviceAccountName: "{{ . }}" 48 | {{- end }} 49 | automountServiceAccountToken: {{ .Values.imageRenderer.automountServiceAccountToken }} 50 | {{- with .Values.imageRenderer.securityContext }} 51 | securityContext: 52 | {{- toYaml . | nindent 8 }} 53 | {{- end }} 54 | {{- with .Values.imageRenderer.hostAliases }} 55 | hostAliases: 56 | {{- toYaml . | nindent 8 }} 57 | {{- end }} 58 | {{- with .Values.imageRenderer.priorityClassName }} 59 | priorityClassName: {{ . }} 60 | {{- end }} 61 | {{- with .Values.imageRenderer.image.pullSecrets }} 62 | imagePullSecrets: 63 | {{- range . }} 64 | - name: {{ tpl . $root }} 65 | {{- end}} 66 | {{- end }} 67 | containers: 68 | - name: {{ .Chart.Name }}-image-renderer 69 | {{- $registry := .Values.global.imageRegistry | default .Values.imageRenderer.image.registry -}} 70 | {{- if .Values.imageRenderer.image.sha }} 71 | image: "{{ $registry }}/{{ .Values.imageRenderer.image.repository }}:{{ .Values.imageRenderer.image.tag }}@sha256:{{ .Values.imageRenderer.image.sha }}" 72 | {{- else }} 73 | image: "{{ $registry }}/{{ .Values.imageRenderer.image.repository }}:{{ .Values.imageRenderer.image.tag }}" 74 | {{- end }} 75 | imagePullPolicy: {{ .Values.imageRenderer.image.pullPolicy }} 76 | {{- if .Values.imageRenderer.command }} 77 | command: 78 | {{- range .Values.imageRenderer.command }} 79 | - {{ . }} 80 | {{- end }} 81 | {{- end}} 82 | ports: 83 | - name: {{ .Values.imageRenderer.service.portName }} 84 | containerPort: {{ .Values.imageRenderer.service.targetPort }} 85 | protocol: TCP 86 | livenessProbe: 87 | httpGet: 88 | path: / 89 | port: {{ .Values.imageRenderer.service.portName }} 90 | env: 91 | - name: HTTP_PORT 92 | value: {{ .Values.imageRenderer.service.targetPort | quote }} 93 | {{- if .Values.imageRenderer.serviceMonitor.enabled }} 94 | - name: ENABLE_METRICS 95 | value: "true" 96 | {{- end }} 97 | {{- range $key, $value := .Values.imageRenderer.envValueFrom }} 98 | - name: {{ $key | quote }} 99 | valueFrom: 100 | {{- tpl (toYaml $value) $ | nindent 16 }} 101 | {{- end }} 102 | {{- range $key, $value := .Values.imageRenderer.env }} 103 | - name: {{ $key | quote }} 104 | value: {{ $value | quote }} 105 | {{- end }} 106 | {{- with .Values.imageRenderer.containerSecurityContext }} 107 | securityContext: 108 | {{- toYaml . | nindent 12 }} 109 | {{- end }} 110 | volumeMounts: 111 | - mountPath: /tmp 112 | name: image-renderer-tmpfs 113 | {{- range .Values.imageRenderer.extraConfigmapMounts }} 114 | - name: {{ tpl .name $root }} 115 | mountPath: {{ tpl .mountPath $root }} 116 | subPath: {{ tpl (.subPath | default "") $root }} 117 | readOnly: {{ .readOnly }} 118 | {{- end }} 119 | {{- range .Values.imageRenderer.extraSecretMounts }} 120 | - name: {{ .name }} 121 | mountPath: {{ .mountPath }} 122 | readOnly: {{ .readOnly }} 123 | subPath: {{ .subPath | default "" }} 124 | {{- end }} 125 | {{- range .Values.imageRenderer.extraVolumeMounts }} 126 | - name: {{ .name }} 127 | mountPath: {{ .mountPath }} 128 | subPath: {{ .subPath | default "" }} 129 | readOnly: {{ .readOnly }} 130 | {{- end }} 131 | {{- with .Values.imageRenderer.resources }} 132 | resources: 133 | {{- toYaml . | nindent 12 }} 134 | {{- end }} 135 | {{- with .Values.imageRenderer.nodeSelector }} 136 | nodeSelector: 137 | {{- toYaml . | nindent 8 }} 138 | {{- end }} 139 | {{- with .Values.imageRenderer.affinity }} 140 | affinity: 141 | {{- tpl (toYaml .) $root | nindent 8 }} 142 | {{- end }} 143 | {{- with .Values.imageRenderer.tolerations }} 144 | tolerations: 145 | {{- toYaml . | nindent 8 }} 146 | {{- end }} 147 | volumes: 148 | - name: image-renderer-tmpfs 149 | emptyDir: {} 150 | {{- range .Values.imageRenderer.extraConfigmapMounts }} 151 | - name: {{ tpl .name $root }} 152 | configMap: 153 | name: {{ tpl .configMap $root }} 154 | {{- with .items }} 155 | items: 156 | {{- toYaml . | nindent 14 }} 157 | {{- end }} 158 | {{- end }} 159 | {{- range .Values.imageRenderer.extraSecretMounts }} 160 | {{- if .secretName }} 161 | - name: {{ .name }} 162 | secret: 163 | secretName: {{ .secretName }} 164 | defaultMode: {{ .defaultMode }} 165 | {{- with .items }} 166 | items: 167 | {{- toYaml . | nindent 14 }} 168 | {{- end }} 169 | {{- else if .projected }} 170 | - name: {{ .name }} 171 | projected: 172 | {{- toYaml .projected | nindent 12 }} 173 | {{- else if .csi }} 174 | - name: {{ .name }} 175 | csi: 176 | {{- toYaml .csi | nindent 12 }} 177 | {{- end }} 178 | {{- end }} 179 | {{- range .Values.imageRenderer.extraVolumes }} 180 | - name: {{ .name }} 181 | {{- if .existingClaim }} 182 | persistentVolumeClaim: 183 | claimName: {{ .existingClaim }} 184 | {{- else if .hostPath }} 185 | hostPath: 186 | {{ toYaml .hostPath | nindent 12 }} 187 | {{- else if .csi }} 188 | csi: 189 | {{- toYaml .csi | nindent 12 }} 190 | {{- else if .configMap }} 191 | configMap: 192 | {{- toYaml .configMap | nindent 12 }} 193 | {{- else if .emptyDir }} 194 | emptyDir: 195 | {{- toYaml .emptyDir | nindent 12 }} 196 | {{- else }} 197 | emptyDir: {} 198 | {{- end }} 199 | {{- end }} 200 | {{- end }} 201 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/image-renderer-hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.imageRenderer.enabled .Values.imageRenderer.autoscaling.enabled }} 2 | apiVersion: {{ include "grafana.hpa.apiVersion" . }} 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "grafana.fullname" . }}-image-renderer 6 | namespace: {{ include "grafana.namespace" . }} 7 | labels: 8 | app.kubernetes.io/name: {{ include "grafana.name" . }}-image-renderer 9 | helm.sh/chart: {{ include "grafana.chart" . }} 10 | app.kubernetes.io/instance: {{ .Release.Name }} 11 | spec: 12 | scaleTargetRef: 13 | apiVersion: apps/v1 14 | kind: Deployment 15 | name: {{ include "grafana.fullname" . }}-image-renderer 16 | minReplicas: {{ .Values.imageRenderer.autoscaling.minReplicas }} 17 | maxReplicas: {{ .Values.imageRenderer.autoscaling.maxReplicas }} 18 | metrics: 19 | {{- if .Values.imageRenderer.autoscaling.targetMemory }} 20 | - type: Resource 21 | resource: 22 | name: memory 23 | {{- if eq (include "grafana.hpa.apiVersion" .) "autoscaling/v2beta1" }} 24 | targetAverageUtilization: {{ .Values.imageRenderer.autoscaling.targetMemory }} 25 | {{- else }} 26 | target: 27 | type: Utilization 28 | averageUtilization: {{ .Values.imageRenderer.autoscaling.targetMemory }} 29 | {{- end }} 30 | {{- end }} 31 | {{- if .Values.imageRenderer.autoscaling.targetCPU }} 32 | - type: Resource 33 | resource: 34 | name: cpu 35 | {{- if eq (include "grafana.hpa.apiVersion" .) "autoscaling/v2beta1" }} 36 | targetAverageUtilization: {{ .Values.imageRenderer.autoscaling.targetCPU }} 37 | {{- else }} 38 | target: 39 | type: Utilization 40 | averageUtilization: {{ .Values.imageRenderer.autoscaling.targetCPU }} 41 | {{- end }} 42 | {{- end }} 43 | {{- if .Values.imageRenderer.autoscaling.behavior }} 44 | behavior: {{ toYaml .Values.imageRenderer.autoscaling.behavior | nindent 4 }} 45 | {{- end }} 46 | {{- end }} 47 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/image-renderer-network-policy.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.imageRenderer.enabled .Values.imageRenderer.networkPolicy.limitIngress }} 2 | --- 3 | apiVersion: networking.k8s.io/v1 4 | kind: NetworkPolicy 5 | metadata: 6 | name: {{ include "grafana.fullname" . }}-image-renderer-ingress 7 | namespace: {{ include "grafana.namespace" . }} 8 | annotations: 9 | comment: Limit image-renderer ingress traffic from grafana 10 | spec: 11 | podSelector: 12 | matchLabels: 13 | {{- include "grafana.imageRenderer.selectorLabels" . | nindent 6 }} 14 | {{- with .Values.imageRenderer.podLabels }} 15 | {{- toYaml . | nindent 6 }} 16 | {{- end }} 17 | 18 | policyTypes: 19 | - Ingress 20 | ingress: 21 | - ports: 22 | - port: {{ .Values.imageRenderer.service.targetPort }} 23 | protocol: TCP 24 | from: 25 | - namespaceSelector: 26 | matchLabels: 27 | kubernetes.io/metadata.name: {{ include "grafana.namespace" . }} 28 | podSelector: 29 | matchLabels: 30 | {{- include "grafana.selectorLabels" . | nindent 14 }} 31 | {{- with .Values.podLabels }} 32 | {{- toYaml . | nindent 14 }} 33 | {{- end }} 34 | {{- with .Values.imageRenderer.networkPolicy.extraIngressSelectors -}} 35 | {{ toYaml . | nindent 8 }} 36 | {{- end }} 37 | {{- end }} 38 | 39 | {{- if and .Values.imageRenderer.enabled .Values.imageRenderer.networkPolicy.limitEgress }} 40 | --- 41 | apiVersion: networking.k8s.io/v1 42 | kind: NetworkPolicy 43 | metadata: 44 | name: {{ include "grafana.fullname" . }}-image-renderer-egress 45 | namespace: {{ include "grafana.namespace" . }} 46 | annotations: 47 | comment: Limit image-renderer egress traffic to grafana 48 | spec: 49 | podSelector: 50 | matchLabels: 51 | {{- include "grafana.imageRenderer.selectorLabels" . | nindent 6 }} 52 | {{- with .Values.imageRenderer.podLabels }} 53 | {{- toYaml . | nindent 6 }} 54 | {{- end }} 55 | 56 | policyTypes: 57 | - Egress 58 | egress: 59 | # allow dns resolution 60 | - ports: 61 | - port: 53 62 | protocol: UDP 63 | - port: 53 64 | protocol: TCP 65 | # talk only to grafana 66 | - ports: 67 | - port: {{ .Values.service.targetPort }} 68 | protocol: TCP 69 | to: 70 | - namespaceSelector: 71 | matchLabels: 72 | kubernetes.io/metadata.name: {{ include "grafana.namespace" . }} 73 | podSelector: 74 | matchLabels: 75 | {{- include "grafana.selectorLabels" . | nindent 14 }} 76 | {{- with .Values.podLabels }} 77 | {{- toYaml . | nindent 14 }} 78 | {{- end }} 79 | {{- end }} 80 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/image-renderer-service.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.imageRenderer.enabled .Values.imageRenderer.service.enabled }} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "grafana.fullname" . }}-image-renderer 6 | namespace: {{ include "grafana.namespace" . }} 7 | labels: 8 | {{- include "grafana.imageRenderer.labels" . | nindent 4 }} 9 | {{- with .Values.imageRenderer.service.labels }} 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- with .Values.imageRenderer.service.annotations }} 13 | annotations: 14 | {{- toYaml . | nindent 4 }} 15 | {{- end }} 16 | spec: 17 | type: ClusterIP 18 | {{- with .Values.imageRenderer.service.clusterIP }} 19 | clusterIP: {{ . }} 20 | {{- end }} 21 | ports: 22 | - name: {{ .Values.imageRenderer.service.portName }} 23 | port: {{ .Values.imageRenderer.service.port }} 24 | protocol: TCP 25 | targetPort: {{ .Values.imageRenderer.service.targetPort }} 26 | {{- with .Values.imageRenderer.appProtocol }} 27 | appProtocol: {{ . }} 28 | {{- end }} 29 | selector: 30 | {{- include "grafana.imageRenderer.selectorLabels" . | nindent 4 }} 31 | {{- end }} 32 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/image-renderer-servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.imageRenderer.serviceMonitor.enabled }} 2 | --- 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | name: {{ include "grafana.fullname" . }}-image-renderer 7 | {{- if .Values.imageRenderer.serviceMonitor.namespace }} 8 | namespace: {{ tpl .Values.imageRenderer.serviceMonitor.namespace . }} 9 | {{- else }} 10 | namespace: {{ include "grafana.namespace" . }} 11 | {{- end }} 12 | labels: 13 | {{- include "grafana.imageRenderer.labels" . | nindent 4 }} 14 | {{- with .Values.imageRenderer.serviceMonitor.labels }} 15 | {{- toYaml . | nindent 4 }} 16 | {{- end }} 17 | spec: 18 | endpoints: 19 | - port: {{ .Values.imageRenderer.service.portName }} 20 | {{- with .Values.imageRenderer.serviceMonitor.interval }} 21 | interval: {{ . }} 22 | {{- end }} 23 | {{- with .Values.imageRenderer.serviceMonitor.scrapeTimeout }} 24 | scrapeTimeout: {{ . }} 25 | {{- end }} 26 | honorLabels: true 27 | path: {{ .Values.imageRenderer.serviceMonitor.path }} 28 | scheme: {{ .Values.imageRenderer.serviceMonitor.scheme }} 29 | {{- with .Values.imageRenderer.serviceMonitor.tlsConfig }} 30 | tlsConfig: 31 | {{- toYaml . | nindent 6 }} 32 | {{- end }} 33 | {{- with .Values.imageRenderer.serviceMonitor.relabelings }} 34 | relabelings: 35 | {{- toYaml . | nindent 6 }} 36 | {{- end }} 37 | jobLabel: "{{ .Release.Name }}-image-renderer" 38 | selector: 39 | matchLabels: 40 | {{- include "grafana.imageRenderer.selectorLabels" . | nindent 6 }} 41 | namespaceSelector: 42 | matchNames: 43 | - {{ include "grafana.namespace" . }} 44 | {{- with .Values.imageRenderer.serviceMonitor.targetLabels }} 45 | targetLabels: 46 | {{- toYaml . | nindent 4 }} 47 | {{- end }} 48 | {{- end }} 49 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $ingressApiIsStable := eq (include "grafana.ingress.isStable" .) "true" -}} 3 | {{- $ingressSupportsIngressClassName := eq (include "grafana.ingress.supportsIngressClassName" .) "true" -}} 4 | {{- $ingressSupportsPathType := eq (include "grafana.ingress.supportsPathType" .) "true" -}} 5 | {{- $fullName := include "grafana.fullname" . -}} 6 | {{- $servicePort := .Values.service.port -}} 7 | {{- $ingressPath := .Values.ingress.path -}} 8 | {{- $ingressPathType := .Values.ingress.pathType -}} 9 | {{- $extraPaths := .Values.ingress.extraPaths -}} 10 | apiVersion: {{ include "grafana.ingress.apiVersion" . }} 11 | kind: Ingress 12 | metadata: 13 | name: {{ $fullName }} 14 | namespace: {{ include "grafana.namespace" . }} 15 | labels: 16 | {{- include "grafana.labels" . | nindent 4 }} 17 | {{- with .Values.ingress.labels }} 18 | {{- toYaml . | nindent 4 }} 19 | {{- end }} 20 | {{- with .Values.ingress.annotations }} 21 | annotations: 22 | {{- range $key, $value := . }} 23 | {{ $key }}: {{ tpl $value $ | quote }} 24 | {{- end }} 25 | {{- end }} 26 | spec: 27 | {{- if and $ingressSupportsIngressClassName .Values.ingress.ingressClassName }} 28 | ingressClassName: {{ .Values.ingress.ingressClassName }} 29 | {{- end -}} 30 | {{- with .Values.ingress.tls }} 31 | tls: 32 | {{- tpl (toYaml .) $ | nindent 4 }} 33 | {{- end }} 34 | rules: 35 | {{- if .Values.ingress.hosts }} 36 | {{- range .Values.ingress.hosts }} 37 | - host: {{ tpl . $ | quote }} 38 | http: 39 | paths: 40 | {{- with $extraPaths }} 41 | {{- toYaml . | nindent 10 }} 42 | {{- end }} 43 | - path: {{ $ingressPath }} 44 | {{- if $ingressSupportsPathType }} 45 | pathType: {{ $ingressPathType }} 46 | {{- end }} 47 | backend: 48 | {{- if $ingressApiIsStable }} 49 | service: 50 | name: {{ $fullName }} 51 | port: 52 | number: {{ $servicePort }} 53 | {{- else }} 54 | serviceName: {{ $fullName }} 55 | servicePort: {{ $servicePort }} 56 | {{- end }} 57 | {{- end }} 58 | {{- else }} 59 | - http: 60 | paths: 61 | - backend: 62 | {{- if $ingressApiIsStable }} 63 | service: 64 | name: {{ $fullName }} 65 | port: 66 | number: {{ $servicePort }} 67 | {{- else }} 68 | serviceName: {{ $fullName }} 69 | servicePort: {{ $servicePort }} 70 | {{- end }} 71 | {{- with $ingressPath }} 72 | path: {{ . }} 73 | {{- end }} 74 | {{- if $ingressSupportsPathType }} 75 | pathType: {{ $ingressPathType }} 76 | {{- end }} 77 | {{- end -}} 78 | {{- end }} 79 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/networkpolicy.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.networkPolicy.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: NetworkPolicy 4 | metadata: 5 | name: {{ include "grafana.fullname" . }} 6 | namespace: {{ include "grafana.namespace" . }} 7 | labels: 8 | {{- include "grafana.labels" . | nindent 4 }} 9 | {{- with .Values.labels }} 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- with .Values.annotations }} 13 | annotations: 14 | {{- toYaml . | nindent 4 }} 15 | {{- end }} 16 | spec: 17 | policyTypes: 18 | {{- if .Values.networkPolicy.ingress }} 19 | - Ingress 20 | {{- end }} 21 | {{- if .Values.networkPolicy.egress.enabled }} 22 | - Egress 23 | {{- end }} 24 | podSelector: 25 | matchLabels: 26 | {{- include "grafana.selectorLabels" . | nindent 6 }} 27 | 28 | {{- if .Values.networkPolicy.egress.enabled }} 29 | egress: 30 | {{- if not .Values.networkPolicy.egress.blockDNSResolution }} 31 | - ports: 32 | - port: 53 33 | protocol: UDP 34 | {{- end }} 35 | - ports: 36 | {{ .Values.networkPolicy.egress.ports | toJson }} 37 | {{- with .Values.networkPolicy.egress.to }} 38 | to: 39 | {{- toYaml . | nindent 12 }} 40 | {{- end }} 41 | {{- end }} 42 | {{- if .Values.networkPolicy.ingress }} 43 | ingress: 44 | - ports: 45 | - port: {{ .Values.service.targetPort }} 46 | {{- if not .Values.networkPolicy.allowExternal }} 47 | from: 48 | - podSelector: 49 | matchLabels: 50 | {{ include "grafana.fullname" . }}-client: "true" 51 | {{- with .Values.networkPolicy.explicitNamespacesSelector }} 52 | - namespaceSelector: 53 | {{- toYaml . | nindent 12 }} 54 | {{- end }} 55 | - podSelector: 56 | matchLabels: 57 | {{- include "grafana.labels" . | nindent 14 }} 58 | role: read 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/poddisruptionbudget.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.podDisruptionBudget }} 2 | apiVersion: {{ include "grafana.podDisruptionBudget.apiVersion" . }} 3 | kind: PodDisruptionBudget 4 | metadata: 5 | name: {{ include "grafana.fullname" . }} 6 | namespace: {{ include "grafana.namespace" . }} 7 | labels: 8 | {{- include "grafana.labels" . | nindent 4 }} 9 | {{- with .Values.labels }} 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | {{- with .Values.podDisruptionBudget.minAvailable }} 14 | minAvailable: {{ . }} 15 | {{- end }} 16 | {{- with .Values.podDisruptionBudget.maxUnavailable }} 17 | maxUnavailable: {{ . }} 18 | {{- end }} 19 | selector: 20 | matchLabels: 21 | {{- include "grafana.selectorLabels" . | nindent 6 }} 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/podsecuritypolicy.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.pspEnabled (.Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy") }} 2 | apiVersion: policy/v1beta1 3 | kind: PodSecurityPolicy 4 | metadata: 5 | name: {{ include "grafana.fullname" . }} 6 | labels: 7 | {{- include "grafana.labels" . | nindent 4 }} 8 | annotations: 9 | seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default,runtime/default' 10 | seccomp.security.alpha.kubernetes.io/defaultProfileName: 'docker/default' 11 | {{- if .Values.rbac.pspUseAppArmor }} 12 | apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default' 13 | apparmor.security.beta.kubernetes.io/defaultProfileName: 'runtime/default' 14 | {{- end }} 15 | spec: 16 | privileged: false 17 | allowPrivilegeEscalation: false 18 | requiredDropCapabilities: 19 | # Default set from Docker, with DAC_OVERRIDE and CHOWN 20 | - ALL 21 | volumes: 22 | - 'configMap' 23 | - 'emptyDir' 24 | - 'projected' 25 | - 'csi' 26 | - 'secret' 27 | - 'downwardAPI' 28 | - 'persistentVolumeClaim' 29 | hostNetwork: false 30 | hostIPC: false 31 | hostPID: false 32 | runAsUser: 33 | rule: 'RunAsAny' 34 | seLinux: 35 | rule: 'RunAsAny' 36 | supplementalGroups: 37 | rule: 'MustRunAs' 38 | ranges: 39 | # Forbid adding the root group. 40 | - min: 1 41 | max: 65535 42 | fsGroup: 43 | rule: 'MustRunAs' 44 | ranges: 45 | # Forbid adding the root group. 46 | - min: 1 47 | max: 65535 48 | readOnlyRootFilesystem: false 49 | {{- end }} 50 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/pvc.yaml: -------------------------------------------------------------------------------- 1 | {{- if and (not .Values.useStatefulSet) .Values.persistence.enabled (not .Values.persistence.existingClaim) (eq .Values.persistence.type "pvc")}} 2 | apiVersion: v1 3 | kind: PersistentVolumeClaim 4 | metadata: 5 | name: {{ include "grafana.fullname" . }} 6 | namespace: {{ include "grafana.namespace" . }} 7 | labels: 8 | {{- include "grafana.labels" . | nindent 4 }} 9 | {{- with .Values.persistence.extraPvcLabels }} 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- with .Values.persistence.annotations }} 13 | annotations: 14 | {{- toYaml . | nindent 4 }} 15 | {{- end }} 16 | {{- with .Values.persistence.finalizers }} 17 | finalizers: 18 | {{- toYaml . | nindent 4 }} 19 | {{- end }} 20 | spec: 21 | accessModes: 22 | {{- range .Values.persistence.accessModes }} 23 | - {{ . | quote }} 24 | {{- end }} 25 | resources: 26 | requests: 27 | storage: {{ .Values.persistence.size | quote }} 28 | {{- if and (.Values.persistence.lookupVolumeName) (lookup "v1" "PersistentVolumeClaim" (include "grafana.namespace" .) (include "grafana.fullname" .)) }} 29 | volumeName: {{ (lookup "v1" "PersistentVolumeClaim" (include "grafana.namespace" .) (include "grafana.fullname" .)).spec.volumeName }} 30 | {{- end }} 31 | {{- with .Values.persistence.storageClassName }} 32 | storageClassName: {{ . }} 33 | {{- end }} 34 | {{- with .Values.persistence.selectorLabels }} 35 | selector: 36 | matchLabels: 37 | {{- toYaml . | nindent 6 }} 38 | {{- end }} 39 | {{- end }} 40 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/role.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create (not .Values.rbac.useExistingRole) -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: {{ include "grafana.fullname" . }} 6 | namespace: {{ include "grafana.namespace" . }} 7 | labels: 8 | {{- include "grafana.labels" . | nindent 4 }} 9 | {{- with .Values.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | {{- if or .Values.rbac.pspEnabled (and .Values.rbac.namespaced (or .Values.sidecar.dashboards.enabled .Values.sidecar.datasources.enabled .Values.sidecar.plugins.enabled .Values.rbac.extraRoleRules)) }} 14 | rules: 15 | {{- if and .Values.rbac.pspEnabled (.Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy") }} 16 | - apiGroups: ['extensions'] 17 | resources: ['podsecuritypolicies'] 18 | verbs: ['use'] 19 | resourceNames: [{{ include "grafana.fullname" . }}] 20 | {{- end }} 21 | {{- if and .Values.rbac.namespaced (or .Values.sidecar.dashboards.enabled .Values.sidecar.datasources.enabled .Values.sidecar.plugins.enabled) }} 22 | - apiGroups: [""] # "" indicates the core API group 23 | resources: ["configmaps", "secrets"] 24 | verbs: ["get", "watch", "list"] 25 | {{- end }} 26 | {{- with .Values.rbac.extraRoleRules }} 27 | {{- toYaml . | nindent 2 }} 28 | {{- end}} 29 | {{- else }} 30 | rules: [] 31 | {{- end }} 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: RoleBinding 4 | metadata: 5 | name: {{ include "grafana.fullname" . }} 6 | namespace: {{ include "grafana.namespace" . }} 7 | labels: 8 | {{- include "grafana.labels" . | nindent 4 }} 9 | {{- with .Values.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | roleRef: 14 | apiGroup: rbac.authorization.k8s.io 15 | kind: Role 16 | {{- if .Values.rbac.useExistingRole }} 17 | name: {{ .Values.rbac.useExistingRole }} 18 | {{- else }} 19 | name: {{ include "grafana.fullname" . }} 20 | {{- end }} 21 | subjects: 22 | - kind: ServiceAccount 23 | name: {{ include "grafana.serviceAccountName" . }} 24 | namespace: {{ include "grafana.namespace" . }} 25 | {{- end }} 26 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/route.yaml: -------------------------------------------------------------------------------- 1 | {{- range $name, $route := .Values.route }} 2 | {{- if $route.enabled -}} 3 | --- 4 | apiVersion: {{ $route.apiVersion | default "gateway.networking.k8s.io/v1" }} 5 | kind: {{ $route.kind | default "HTTPRoute" }} 6 | metadata: 7 | {{- with $route.annotations }} 8 | annotations: 9 | {{- toYaml . | nindent 4 }} 10 | {{- end }} 11 | name: {{ template "grafana.fullname" $ }}{{ if ne $name "main" }}-{{ $name }}{{ end }} 12 | namespace: {{ template "grafana.namespace" $ }} 13 | labels: 14 | app: {{ template "grafana.name" $ }}-prometheus 15 | {{- include "grafana.labels" $ | nindent 4 }} 16 | {{- with $route.labels }} 17 | {{- toYaml . | nindent 4 }} 18 | {{- end }} 19 | spec: 20 | {{- with $route.parentRefs }} 21 | parentRefs: 22 | {{- toYaml . | nindent 4 }} 23 | {{- end }} 24 | {{- with $route.hostnames }} 25 | hostnames: 26 | {{- tpl (toYaml .) $ | nindent 4 }} 27 | {{- end }} 28 | rules: 29 | {{- if $route.additionalRules }} 30 | {{- tpl (toYaml $route.additionalRules) $ | nindent 4 }} 31 | {{- end }} 32 | - backendRefs: 33 | - name: {{ include "grafana.fullname" $ }} 34 | port: {{ $.Values.service.port }} 35 | {{- with $route.filters }} 36 | filters: 37 | {{- toYaml . | nindent 8 }} 38 | {{- end }} 39 | {{- with $route.matches }} 40 | matches: 41 | {{- toYaml . | nindent 8 }} 42 | {{- end }} 43 | {{- end }} 44 | {{- end }} 45 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/secret-env.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.envRenderSecret }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ include "grafana.fullname" . }}-env 6 | namespace: {{ include "grafana.namespace" . }} 7 | labels: 8 | {{- include "grafana.labels" . | nindent 4 }} 9 | type: Opaque 10 | data: 11 | {{- range $key, $val := .Values.envRenderSecret }} 12 | {{ $key }}: {{ tpl ($val | toString) $ | b64enc | quote }} 13 | {{- end }} 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if or (and (not .Values.admin.existingSecret) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION)) (and .Values.ldap.enabled (not .Values.ldap.existingSecret)) }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ include "grafana.fullname" . }} 6 | namespace: {{ include "grafana.namespace" . }} 7 | labels: 8 | {{- include "grafana.labels" . | nindent 4 }} 9 | {{- with .Values.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | type: Opaque 14 | data: 15 | {{- include "grafana.secretsData" . | nindent 2 }} 16 | {{- end }} 17 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.service.enabled }} 2 | {{- $root := . }} 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: {{ include "grafana.fullname" . }} 7 | namespace: {{ include "grafana.namespace" . }} 8 | labels: 9 | {{- include "grafana.labels" . | nindent 4 }} 10 | {{- with .Values.service.labels }} 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | {{- with .Values.service.annotations }} 14 | annotations: 15 | {{- tpl (toYaml . | nindent 4) $root }} 16 | {{- end }} 17 | spec: 18 | {{- if (or (eq .Values.service.type "ClusterIP") (empty .Values.service.type)) }} 19 | type: ClusterIP 20 | {{- with .Values.service.clusterIP }} 21 | clusterIP: {{ . }} 22 | {{- end }} 23 | {{- else if eq .Values.service.type "LoadBalancer" }} 24 | type: LoadBalancer 25 | {{- with .Values.service.loadBalancerIP }} 26 | loadBalancerIP: {{ . }} 27 | {{- end }} 28 | {{- with .Values.service.loadBalancerClass }} 29 | loadBalancerClass: {{ . }} 30 | {{- end }} 31 | {{- with .Values.service.loadBalancerSourceRanges }} 32 | loadBalancerSourceRanges: 33 | {{- toYaml . | nindent 4 }} 34 | {{- end }} 35 | {{- else }} 36 | type: {{ .Values.service.type }} 37 | {{- end }} 38 | {{- if .Values.service.ipFamilyPolicy }} 39 | ipFamilyPolicy: {{ .Values.service.ipFamilyPolicy }} 40 | {{- end }} 41 | {{- if .Values.service.ipFamilies }} 42 | ipFamilies: {{ .Values.service.ipFamilies | toYaml | nindent 2 }} 43 | {{- end }} 44 | {{- with .Values.service.externalIPs }} 45 | externalIPs: 46 | {{- toYaml . | nindent 4 }} 47 | {{- end }} 48 | {{- with .Values.service.externalTrafficPolicy }} 49 | externalTrafficPolicy: {{ . }} 50 | {{- end }} 51 | {{- with .Values.service.sessionAffinity }} 52 | sessionAffinity: {{ . }} 53 | {{- end }} 54 | ports: 55 | - name: {{ .Values.service.portName }} 56 | port: {{ .Values.service.port }} 57 | protocol: TCP 58 | targetPort: {{ .Values.service.targetPort }} 59 | {{- with .Values.service.appProtocol }} 60 | appProtocol: {{ . }} 61 | {{- end }} 62 | {{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort))) }} 63 | nodePort: {{ .Values.service.nodePort }} 64 | {{- end }} 65 | {{- with .Values.extraExposePorts }} 66 | {{- tpl (toYaml . | nindent 4) $root }} 67 | {{- end }} 68 | selector: 69 | {{- include "grafana.selectorLabels" . | nindent 4 }} 70 | {{- end }} 71 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | automountServiceAccountToken: {{ .Values.serviceAccount.autoMount | default .Values.serviceAccount.automountServiceAccountToken }} 5 | metadata: 6 | labels: 7 | {{- include "grafana.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.labels }} 9 | {{- toYaml . | nindent 4 }} 10 | {{- end }} 11 | {{- with .Values.serviceAccount.annotations }} 12 | annotations: 13 | {{- tpl (toYaml . | nindent 4) $ }} 14 | {{- end }} 15 | name: {{ include "grafana.serviceAccountName" . }} 16 | namespace: {{ include "grafana.namespace" . }} 17 | {{- end }} 18 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceMonitor.enabled }} 2 | --- 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | name: {{ include "grafana.fullname" . }} 7 | {{- if .Values.serviceMonitor.namespace }} 8 | namespace: {{ tpl .Values.serviceMonitor.namespace . }} 9 | {{- else }} 10 | namespace: {{ include "grafana.namespace" . }} 11 | {{- end }} 12 | labels: 13 | {{- include "grafana.labels" . | nindent 4 }} 14 | {{- with .Values.serviceMonitor.labels }} 15 | {{- tpl (toYaml . | nindent 4) $ }} 16 | {{- end }} 17 | spec: 18 | endpoints: 19 | - port: {{ .Values.service.portName }} 20 | {{- with .Values.serviceMonitor.interval }} 21 | interval: {{ . }} 22 | {{- end }} 23 | {{- with .Values.serviceMonitor.scrapeTimeout }} 24 | scrapeTimeout: {{ . }} 25 | {{- end }} 26 | honorLabels: true 27 | path: {{ .Values.serviceMonitor.path }} 28 | scheme: {{ .Values.serviceMonitor.scheme }} 29 | {{- with .Values.serviceMonitor.tlsConfig }} 30 | tlsConfig: 31 | {{- toYaml . | nindent 6 }} 32 | {{- end }} 33 | {{- with .Values.serviceMonitor.relabelings }} 34 | relabelings: 35 | {{- toYaml . | nindent 6 }} 36 | {{- end }} 37 | {{- with .Values.serviceMonitor.metricRelabelings }} 38 | metricRelabelings: 39 | {{- toYaml . | nindent 6 }} 40 | {{- end }} 41 | {{- with .Values.serviceMonitor.basicAuth }} 42 | basicAuth: 43 | {{- toYaml . | nindent 6 }} 44 | {{- end }} 45 | jobLabel: "{{ .Release.Name }}" 46 | selector: 47 | matchLabels: 48 | {{- include "grafana.selectorLabels" . | nindent 6 }} 49 | namespaceSelector: 50 | matchNames: 51 | - {{ include "grafana.namespace" . }} 52 | {{- with .Values.serviceMonitor.targetLabels }} 53 | targetLabels: 54 | {{- toYaml . | nindent 4 }} 55 | {{- end }} 56 | {{- end }} 57 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/statefulset.yaml: -------------------------------------------------------------------------------- 1 | {{- $sts := list "sts" "StatefulSet" "statefulset" -}} 2 | {{- if (or (.Values.useStatefulSet) (and .Values.persistence.enabled (not .Values.persistence.existingClaim) (has .Values.persistence.type $sts)))}} 3 | apiVersion: apps/v1 4 | kind: StatefulSet 5 | metadata: 6 | name: {{ include "grafana.fullname" . }} 7 | namespace: {{ include "grafana.namespace" . }} 8 | labels: 9 | {{- include "grafana.labels" . | nindent 4 }} 10 | {{- with .Values.annotations }} 11 | annotations: 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | spec: 15 | replicas: {{ .Values.replicas }} 16 | selector: 17 | matchLabels: 18 | {{- include "grafana.selectorLabels" . | nindent 6 }} 19 | serviceName: {{ include "grafana.fullname" . }}-headless 20 | template: 21 | metadata: 22 | labels: 23 | {{- include "grafana.labels" . | nindent 8 }} 24 | {{- with .Values.podLabels }} 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | annotations: 28 | checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} 29 | checksum/dashboards-json-config: {{ include (print $.Template.BasePath "/dashboards-json-configmap.yaml") . | sha256sum }} 30 | checksum/sc-dashboard-provider-config: {{ include (print $.Template.BasePath "/configmap-dashboard-provider.yaml") . | sha256sum }} 31 | {{- if and (or (and (not .Values.admin.existingSecret) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD__FILE) (not .Values.env.GF_SECURITY_ADMIN_PASSWORD)) (and .Values.ldap.enabled (not .Values.ldap.existingSecret))) (not .Values.env.GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION) }} 32 | checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} 33 | {{- end }} 34 | kubectl.kubernetes.io/default-container: {{ .Chart.Name }} 35 | {{- with .Values.podAnnotations }} 36 | {{- toYaml . | nindent 8 }} 37 | {{- end }} 38 | spec: 39 | {{- include "grafana.pod" . | nindent 6 }} 40 | {{- if .Values.persistence.enabled}} 41 | volumeClaimTemplates: 42 | - apiVersion: v1 43 | kind: PersistentVolumeClaim 44 | metadata: 45 | name: storage 46 | spec: 47 | accessModes: {{ .Values.persistence.accessModes }} 48 | storageClassName: {{ .Values.persistence.storageClassName }} 49 | resources: 50 | requests: 51 | storage: {{ .Values.persistence.size }} 52 | {{- with .Values.persistence.selectorLabels }} 53 | selector: 54 | matchLabels: 55 | {{- toYaml . | nindent 10 }} 56 | {{- end }} 57 | {{- end }} 58 | {{- end }} 59 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/tests/test-configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.testFramework.enabled }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ include "grafana.fullname" . }}-test 6 | namespace: {{ include "grafana.namespace" . }} 7 | annotations: 8 | "helm.sh/hook": {{ .Values.testFramework.hookType | default "test" }} 9 | "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" 10 | labels: 11 | {{- include "grafana.labels" . | nindent 4 }} 12 | data: 13 | run.sh: |- 14 | @test "Test Health" { 15 | url="http://{{ include "grafana.fullname" . }}/api/health" 16 | 17 | code=$(wget --server-response --spider --timeout 90 --tries 10 ${url} 2>&1 | awk '/^ HTTP/{print $2}') 18 | [ "$code" == "200" ] 19 | } 20 | {{- end }} 21 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/tests/test-podsecuritypolicy.yaml: -------------------------------------------------------------------------------- 1 | {{- if and (.Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy") .Values.testFramework.enabled .Values.rbac.pspEnabled }} 2 | apiVersion: policy/v1beta1 3 | kind: PodSecurityPolicy 4 | metadata: 5 | name: {{ include "grafana.fullname" . }}-test 6 | annotations: 7 | "helm.sh/hook": {{ .Values.testFramework.hookType | default "test" }} 8 | "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" 9 | labels: 10 | {{- include "grafana.labels" . | nindent 4 }} 11 | spec: 12 | allowPrivilegeEscalation: true 13 | privileged: false 14 | hostNetwork: false 15 | hostIPC: false 16 | hostPID: false 17 | fsGroup: 18 | rule: RunAsAny 19 | seLinux: 20 | rule: RunAsAny 21 | supplementalGroups: 22 | rule: RunAsAny 23 | runAsUser: 24 | rule: RunAsAny 25 | volumes: 26 | - configMap 27 | - downwardAPI 28 | - emptyDir 29 | - projected 30 | - csi 31 | - secret 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/tests/test-role.yaml: -------------------------------------------------------------------------------- 1 | {{- if and (.Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy") .Values.testFramework.enabled .Values.rbac.pspEnabled }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: {{ include "grafana.fullname" . }}-test 6 | namespace: {{ include "grafana.namespace" . }} 7 | annotations: 8 | "helm.sh/hook": {{ .Values.testFramework.hookType | default "test" }} 9 | "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" 10 | labels: 11 | {{- include "grafana.labels" . | nindent 4 }} 12 | rules: 13 | - apiGroups: ['policy'] 14 | resources: ['podsecuritypolicies'] 15 | verbs: ['use'] 16 | resourceNames: [{{ include "grafana.fullname" . }}-test] 17 | {{- end }} 18 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/tests/test-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if and (.Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy") .Values.testFramework.enabled .Values.rbac.pspEnabled }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: RoleBinding 4 | metadata: 5 | name: {{ include "grafana.fullname" . }}-test 6 | namespace: {{ include "grafana.namespace" . }} 7 | annotations: 8 | "helm.sh/hook": {{ .Values.testFramework.hookType | default "test" }} 9 | "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" 10 | labels: 11 | {{- include "grafana.labels" . | nindent 4 }} 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: {{ include "grafana.fullname" . }}-test 16 | subjects: 17 | - kind: ServiceAccount 18 | name: {{ include "grafana.serviceAccountNameTest" . }} 19 | namespace: {{ include "grafana.namespace" . }} 20 | {{- end }} 21 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/tests/test-serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.testFramework.enabled .Values.serviceAccount.create }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | labels: 6 | {{- include "grafana.labels" . | nindent 4 }} 7 | name: {{ include "grafana.serviceAccountNameTest" . }} 8 | namespace: {{ include "grafana.namespace" . }} 9 | annotations: 10 | "helm.sh/hook": {{ .Values.testFramework.hookType | default "test" }} 11 | "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/grafana/templates/tests/test.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.testFramework.enabled }} 2 | {{- $root := . }} 3 | apiVersion: v1 4 | kind: Pod 5 | metadata: 6 | name: {{ include "grafana.fullname" . }}-test 7 | labels: 8 | {{- include "grafana.labels" . | nindent 4 }} 9 | annotations: 10 | "helm.sh/hook": {{ .Values.testFramework.hookType | default "test" }} 11 | "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" 12 | namespace: {{ include "grafana.namespace" . }} 13 | spec: 14 | serviceAccountName: {{ include "grafana.serviceAccountNameTest" . }} 15 | {{- with .Values.testFramework.securityContext }} 16 | securityContext: 17 | {{- toYaml . | nindent 4 }} 18 | {{- end }} 19 | {{- if or .Values.image.pullSecrets .Values.global.imagePullSecrets }} 20 | imagePullSecrets: 21 | {{- include "grafana.imagePullSecrets" (dict "root" $root "imagePullSecrets" .Values.image.pullSecrets) | nindent 4 }} 22 | {{- end }} 23 | {{- with .Values.nodeSelector }} 24 | nodeSelector: 25 | {{- toYaml . | nindent 4 }} 26 | {{- end }} 27 | {{- with .Values.affinity }} 28 | affinity: 29 | {{- tpl (toYaml .) $root | nindent 4 }} 30 | {{- end }} 31 | {{- with .Values.tolerations }} 32 | tolerations: 33 | {{- toYaml . | nindent 4 }} 34 | {{- end }} 35 | containers: 36 | - name: {{ .Release.Name }}-test 37 | image: "{{ .Values.global.imageRegistry | default .Values.testFramework.image.registry }}/{{ .Values.testFramework.image.repository }}:{{ .Values.testFramework.image.tag }}" 38 | imagePullPolicy: "{{ .Values.testFramework.imagePullPolicy}}" 39 | command: ["/opt/bats/bin/bats", "-t", "/tests/run.sh"] 40 | volumeMounts: 41 | - mountPath: /tests 42 | name: tests 43 | readOnly: true 44 | {{- with .Values.testFramework.resources }} 45 | resources: 46 | {{- toYaml . | nindent 8 }} 47 | {{- end }} 48 | volumes: 49 | - name: tests 50 | configMap: 51 | name: {{ include "grafana.fullname" . }}-test 52 | restartPolicy: Never 53 | {{- end }} 54 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/charts/prometheus-27.5.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emylincon/aws_quota_exporter/82caffb48439387e4e9714b3cc6a0e45066e5d9c/kubernetes/helm/aqe/charts/prometheus-27.5.1.tgz -------------------------------------------------------------------------------- /kubernetes/helm/aqe/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "aqe.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "aqe.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "aqe.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "aqe.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "aqe.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "aqe.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "aqe.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "aqe.labels" -}} 37 | helm.sh/chart: {{ include "aqe.chart" . }} 38 | {{ include "aqe.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "aqe.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "aqe.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "aqe.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "aqe.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "aqe.fullname" . }} 5 | labels: 6 | {{- include "aqe.labels" . | nindent 4 }} 7 | {{- with .Values.configmap }} 8 | data: 9 | {{- toYaml . | nindent 2 }} 10 | {{- end }} 11 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "aqe.fullname" . }} 5 | labels: 6 | {{- include "aqe.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "aqe.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "aqe.selectorLabels" . | nindent 8 }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | serviceAccountName: {{ include "aqe.serviceAccountName" . }} 28 | securityContext: 29 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 30 | containers: 31 | - name: {{ .Chart.Name }} 32 | securityContext: 33 | {{- toYaml .Values.securityContext | nindent 12 }} 34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | ports: 37 | - name: http 38 | containerPort: 10100 39 | protocol: TCP 40 | {{- with .Values.livenessProbe }} 41 | livenessProbe: 42 | {{- toYaml . | nindent 12 }} 43 | {{- end }} 44 | {{- with .Values.readinessProbe }} 45 | readinessProbe: 46 | {{- toYaml . | nindent 12 }} 47 | {{- end }} 48 | volumeMounts: 49 | - name: {{ include "aqe.fullname" . }} 50 | mountPath: /etc/aqe/config.yml 51 | subPath: config.yml 52 | args: 53 | {{- toYaml .Values.podArgs | nindent 12 }} 54 | env: 55 | - name: AWS_REGION 56 | value: "{{ .Values.env.AWS_REGION }}" 57 | {{- if and .Values.secret (and (.Values.secret.AWS_ACCESS_KEY_ID) (.Values.secret.AWS_SECRET_ACCESS_KEY)) }} 58 | envFrom: 59 | - secretRef: 60 | name: {{ include "aqe.fullname" . }} 61 | {{- end }} 62 | resources: 63 | {{- toYaml .Values.resources | nindent 12 }} 64 | {{- with .Values.nodeSelector }} 65 | nodeSelector: 66 | {{- toYaml . | nindent 8 }} 67 | {{- end }} 68 | {{- with .Values.affinity }} 69 | affinity: 70 | {{- toYaml . | nindent 8 }} 71 | {{- end }} 72 | {{- with .Values.tolerations }} 73 | tolerations: 74 | {{- toYaml . | nindent 8 }} 75 | {{- end }} 76 | volumes: 77 | - name: {{ include "aqe.fullname" . }} 78 | configMap: 79 | name: {{ include "aqe.fullname" . }} 80 | items: 81 | - key: config.yml 82 | path: config.yml 83 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "aqe.fullname" . }} 6 | labels: 7 | {{- include "aqe.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "aqe.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "aqe.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "aqe.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.secret (and (.Values.secret.AWS_ACCESS_KEY_ID) (.Values.secret.AWS_SECRET_ACCESS_KEY)) }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ include "aqe.fullname" . }} 6 | labels: 7 | {{- include "aqe.labels" . | nindent 4 }} 8 | type: Opaque 9 | {{- with .Values.secret }} 10 | data: 11 | {{- toYaml . | nindent 2 }} 12 | {{- end }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "aqe.fullname" . }} 5 | labels: 6 | {{- include "aqe.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "aqe.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "aqe.serviceAccountName" . }} 6 | labels: 7 | {{- include "aqe.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{ if and .Values.serviceMonitor.create (.Capabilities.APIVersions.Has "monitoring.coreos.com/v1") }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: {{ include "aqe.fullname" . }} 6 | labels: 7 | {{- include "aqe.labels" . | nindent 4 }} 8 | {{- range $key, $value := .Values.serviceMonitor.additionalLabels }} 9 | {{ $key }}: {{ $value | quote }} 10 | {{- end }} 11 | {{- with .Values.serviceMonitor.namespace }} 12 | namespace: {{ . }} 13 | {{- end }} 14 | spec: 15 | endpoints: 16 | - port: http 17 | honorLabels: true 18 | {{- with .Values.serviceMonitor.interval }} 19 | interval: {{ . }} 20 | {{- end }} 21 | {{- with .Values.serviceMonitor.scrapeTimeout }} 22 | scrapeTimeout: {{ . }} 23 | {{- end }} 24 | {{- if .Values.serviceMonitor.metricRelabelings }} 25 | metricRelabelings: 26 | {{- toYaml .Values.serviceMonitor.metricRelabelings | nindent 4 }} 27 | {{- end }} 28 | {{- if .Values.serviceMonitor.relabelings }} 29 | relabelings: 30 | {{- toYaml .Values.serviceMonitor.relabelings | nindent 4 }} 31 | {{- end }} 32 | namespaceSelector: 33 | matchNames: 34 | - {{ .Release.Namespace }} 35 | selector: 36 | matchLabels: 37 | {{- include "aqe.selectorLabels" . | nindent 6 }} 38 | {{- end }} 39 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "aqe.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "aqe.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "aqe.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /kubernetes/helm/aqe/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for aqe. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: ugwuanyi/aqe 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: latest 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: false 20 | # Annotations to add to the service account 21 | annotations: {} 22 | # The name of the service account to use. 23 | # If not set and create is true, a name is generated using the fullname template 24 | name: "" 25 | 26 | podAnnotations: {} 27 | 28 | podSecurityContext: 29 | {} 30 | # fsGroup: 2000 31 | 32 | securityContext: 33 | {} 34 | # capabilities: 35 | # drop: 36 | # - ALL 37 | # readOnlyRootFilesystem: true 38 | # runAsNonRoot: true 39 | # runAsUser: 1000 40 | 41 | livenessProbe: 42 | httpGet: 43 | path: / 44 | port: http 45 | initialDelaySeconds: 60 # might need increasing when a large number of jobs is specified 46 | readinessProbe: 47 | httpGet: 48 | path: / 49 | port: http 50 | initialDelaySeconds: 60 # might need increasing when a large number of jobs is specified 51 | 52 | service: 53 | type: ClusterIP 54 | port: 10100 55 | 56 | ingress: 57 | enabled: false 58 | className: "" 59 | annotations: 60 | {} 61 | # kubernetes.io/ingress.class: nginx 62 | # kubernetes.io/tls-acme: "true" 63 | hosts: 64 | - host: aqe.chart.emylincon.com 65 | paths: 66 | - path: / 67 | pathType: Prefix 68 | tls: [] 69 | # - secretName: chart-example-tls 70 | # hosts: 71 | # - chart-example.local 72 | 73 | resources: 74 | {} 75 | # We usually recommend not to specify default resources and to leave this as a conscious 76 | # choice for the user. This also increases chances charts run on environments with little 77 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 78 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 79 | # limits: 80 | # cpu: 100m 81 | # memory: 128Mi 82 | # requests: 83 | # cpu: 100m 84 | # memory: 128Mi 85 | 86 | autoscaling: 87 | enabled: false 88 | minReplicas: 1 89 | maxReplicas: 100 90 | targetCPUUtilizationPercentage: 80 91 | # targetMemoryUtilizationPercentage: 80 92 | 93 | nodeSelector: {} 94 | 95 | tolerations: [] 96 | 97 | affinity: {} 98 | 99 | configmap: 100 | config.yml: | 101 | jobs: 102 | - serviceCode: lambda 103 | regions: 104 | - us-west-2 105 | - us-east-2 106 | - serviceCode: cloudformation 107 | regions: 108 | - us-west-2 109 | - us-east-2 110 | - serviceCode: ec2 111 | regions: 112 | - us-west-2 113 | - us-east-2 114 | 115 | env: 116 | AWS_REGION: us-west-2 117 | 118 | podArgs: 119 | - --log.level=info 120 | - --log.format=text 121 | 122 | secret: 123 | # base64 encoded secrets (leave empty when using OIDC / kiam / kube2iam etc) 124 | AWS_ACCESS_KEY_ID: "" 125 | AWS_SECRET_ACCESS_KEY: "" 126 | 127 | serviceMonitor: 128 | # Specifies whether a ServiceMonitor should be created 129 | create: false 130 | interval: 131 | scrapeTimeout: 132 | namespace: 133 | additionalLabels: {} 134 | metricRelabelings: [] 135 | relabelings: [] 136 | 137 | prometheus: 138 | enabled: true 139 | serverFiles: 140 | prometheus.yml: 141 | scrape_configs: 142 | # The job name is added as a label `job=` to any timeseries scraped from this config. 143 | - job_name: prometheus 144 | # Override the global default and scrape targets from this job every 5 seconds. 145 | scrape_interval: 5s 146 | static_configs: 147 | - targets: ["localhost:9090"] 148 | - job_name: "aws_quota_exporter" 149 | # Override the global default and scrape targets from this job every 5 seconds. 150 | scrape_interval: 15s 151 | static_configs: 152 | - targets: ["aqe.default.svc.cluster.local:10100"] 153 | kube-state-metrics: 154 | enabled: false 155 | prometheus-node-exporter: 156 | enabled: false 157 | prometheus-pushgateway: 158 | enabled: false 159 | alertmanager: 160 | enabled: false 161 | 162 | grafana: 163 | enabled: true 164 | datasources: 165 | datasources.yaml: 166 | apiVersion: 1 167 | datasources: 168 | - name: prometheus 169 | type: prometheus 170 | # Access mode - proxy (server in the UI) or direct (browser in the UI). 171 | access: proxy 172 | url: http://aqe-prometheus-server.default.svc.cluster.local:80 173 | orgId: 1 174 | editable: true 175 | dashboardProviders: 176 | dashboardproviders.yaml: 177 | apiVersion: 1 178 | providers: 179 | - name: "prometheus" 180 | orgId: 1 181 | folder: "" 182 | type: file 183 | disableDeletion: false 184 | editable: true 185 | options: 186 | path: /var/lib/grafana/dashboards/prometheus 187 | dashboards: 188 | prometheus: 189 | quotas: 190 | file: dashboards/quotas.json 191 | grafana.ini: 192 | auth.basic: 193 | enabled: false 194 | auth.grafana_com: 195 | enabled: false 196 | auth.anonymous: 197 | enabled: true 198 | org_name: true 199 | org_role: Admin 200 | hide_version: true 201 | # adminUser: admin 202 | # adminPassword: admin 203 | -------------------------------------------------------------------------------- /kubernetes/manifests/Taskfile.yaml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: "3" 4 | 5 | includes: 6 | aqe: 7 | taskfile: ./aqe 8 | argo: 9 | taskfile: ./argocd 10 | -------------------------------------------------------------------------------- /kubernetes/manifests/aqe/Taskfile.yaml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: "3" 4 | 5 | tasks: 6 | up: 7 | desc: "deploy manifest files using kubectl" 8 | cmds: 9 | - kubectl apply -f 'kubernetes/manifests/aqe/*.yml' 10 | secret: 11 | desc: "create aws secrets in kubernetes" 12 | cmds: 13 | - kubectl create secret generic aws-secrets --from-env-file=env.out 14 | down: 15 | desc: "delete kubernetes resources" 16 | cmds: 17 | - kubectl delete -f 'kubernetes/manifests/aqe/*.yml' 18 | grafana: 19 | desc: "open grafana webui" 20 | cmds: 21 | - minikube service grafana 22 | -------------------------------------------------------------------------------- /kubernetes/manifests/aqe/aws_quota_exporter.yml: -------------------------------------------------------------------------------- 1 | # https://kubernetes.io/docs/concepts/configuration/configmap/ 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: aqe-configmap 6 | namespace: default 7 | data: 8 | config.yml: | 9 | jobs: 10 | - serviceCode: lambda 11 | regions: 12 | - us-west-1 13 | - us-east-1 14 | - serviceCode: cloudformation 15 | regions: 16 | - us-west-1 17 | - us-east-1 18 | 19 | --- 20 | # https://kubernetes.io/docs/concepts/workloads/controllers/deployment/ 21 | apiVersion: apps/v1 22 | kind: Deployment 23 | metadata: 24 | name: aqe 25 | namespace: default 26 | labels: 27 | app: aqe 28 | spec: 29 | selector: 30 | matchLabels: 31 | app: aqe 32 | replicas: 1 33 | strategy: 34 | rollingUpdate: 35 | maxSurge: 25% 36 | maxUnavailable: 25% 37 | type: RollingUpdate 38 | template: 39 | metadata: 40 | labels: 41 | app: aqe 42 | spec: 43 | containers: 44 | - name: aqe 45 | image: ugwuanyi/aqe:main 46 | imagePullPolicy: IfNotPresent 47 | resources: 48 | requests: 49 | cpu: "1" 50 | memory: 2Gi 51 | limits: 52 | cpu: "1" 53 | memory: 2Gi 54 | 55 | ports: 56 | - containerPort: 10100 57 | name: aqe 58 | 59 | volumeMounts: 60 | - name: aqe-configmap-volume 61 | mountPath: /etc/aqe/config.yml 62 | subPath: config.yml 63 | envFrom: 64 | - secretRef: 65 | name: aws-secrets 66 | 67 | volumes: 68 | - name: aqe-configmap-volume 69 | configMap: 70 | name: aqe-configmap 71 | items: 72 | - key: config.yml 73 | path: config.yml 74 | restartPolicy: Always 75 | --- 76 | # https://kubernetes.io/docs/concepts/services-networking/service/ 77 | apiVersion: v1 78 | kind: Service 79 | metadata: 80 | name: aqe 81 | namespace: default 82 | spec: 83 | selector: 84 | app: aqe 85 | type: LoadBalancer 86 | ports: 87 | - name: aqe 88 | protocol: TCP 89 | port: 10100 90 | targetPort: 10100 91 | --- 92 | -------------------------------------------------------------------------------- /kubernetes/manifests/aqe/prometheus.yml: -------------------------------------------------------------------------------- 1 | # https://kubernetes.io/docs/concepts/configuration/configmap/ 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: prometheus-configmap 6 | namespace: default 7 | data: 8 | prometheus.yml: | 9 | global: 10 | scrape_interval: 15s # By default, scrape targets every 15 seconds. 11 | 12 | # A scrape configuration containing exactly one endpoint to scrape: 13 | # Here it's Prometheus itself. 14 | scrape_configs: 15 | # The job name is added as a label `job=` to any timeseries scraped from this config. 16 | - job_name: "prometheus" 17 | 18 | # Override the global default and scrape targets from this job every 5 seconds. 19 | scrape_interval: 5s 20 | 21 | static_configs: 22 | - targets: ["localhost:9090"] 23 | 24 | - job_name: "aws_quota_exporter" 25 | 26 | # Override the global default and scrape targets from this job every 5 seconds. 27 | scrape_interval: 15s 28 | 29 | static_configs: 30 | - targets: ["aqe.default.svc.cluster.local:10100"] 31 | --- 32 | # https://kubernetes.io/docs/concepts/workloads/controllers/deployment/ 33 | apiVersion: apps/v1 34 | kind: Deployment 35 | metadata: 36 | name: prometheus 37 | namespace: default 38 | labels: 39 | app: prometheus 40 | spec: 41 | selector: 42 | matchLabels: 43 | app: prometheus 44 | replicas: 1 45 | strategy: 46 | rollingUpdate: 47 | maxSurge: 25% 48 | maxUnavailable: 25% 49 | type: RollingUpdate 50 | template: 51 | metadata: 52 | labels: 53 | app: prometheus 54 | spec: 55 | containers: 56 | - name: prometheus 57 | image: prom/prometheus:latest 58 | imagePullPolicy: IfNotPresent 59 | resources: 60 | requests: 61 | cpu: "1" 62 | memory: 2Gi 63 | limits: 64 | cpu: "1" 65 | memory: 2Gi 66 | 67 | ports: 68 | - containerPort: 9090 69 | name: prometheus1 70 | 71 | volumeMounts: 72 | - name: prometheus-configmap-volume 73 | mountPath: /etc/prometheus/prometheus.yml 74 | subPath: prometheus.yml 75 | 76 | volumes: 77 | - name: prometheus-configmap-volume 78 | configMap: 79 | name: prometheus-configmap 80 | items: 81 | - key: prometheus.yml 82 | path: prometheus.yml 83 | restartPolicy: Always 84 | --- 85 | # https://kubernetes.io/docs/concepts/services-networking/service/ 86 | apiVersion: v1 87 | kind: Service 88 | metadata: 89 | name: prometheus 90 | namespace: default 91 | spec: 92 | selector: 93 | app: prometheus 94 | type: LoadBalancer 95 | ports: 96 | - name: prometheus 97 | protocol: TCP 98 | port: 9090 99 | targetPort: 9090 100 | --- 101 | -------------------------------------------------------------------------------- /kubernetes/manifests/argocd/Taskfile.yaml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev 2 | 3 | version: "3" 4 | 5 | tasks: 6 | up: 7 | desc: "deploy argocd" 8 | cmds: 9 | - kubectl apply -n argocd -f kubernetes/manifests/argocd/install.yaml 10 | down: 11 | desc: "delete argocd resources" 12 | cmds: 13 | - kubectl delete -n argocd -f kubernetes/manifests/argocd/install.yaml 14 | password: 15 | desc: "get argocd password" 16 | cmds: 17 | - kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{{`{.data.password}`}}' | base64 -d | pbcopy 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Package main implements the AWS Quota Exporter (AQE), a tool for exporting AWS service quotas 2 | // as Prometheus metrics. The application provides functionality to scrape AWS service quota 3 | // information, expose it as Prometheus metrics, and serve it over HTTP. 4 | // 5 | // The main package includes the following features: 6 | // - Command-line flags for configuration, including log settings, Prometheus port, and cache duration. 7 | // - Graceful shutdown handling using OS signals. 8 | // - Build information exposure as Prometheus metrics and version display. 9 | // - Integration with Prometheus for metrics collection and HTTP serving. 10 | // 11 | // The application is designed to be extensible and configurable, allowing users to customize 12 | // logging, caching, and metric collection behavior. 13 | package main 14 | 15 | import ( 16 | "flag" 17 | "fmt" 18 | "net/http" 19 | "os" 20 | "os/signal" 21 | "runtime" 22 | "syscall" 23 | "time" 24 | 25 | "github.com/aws/aws-sdk-go/aws/awsutil" 26 | "github.com/emylincon/aws_quota_exporter/pkg" 27 | "github.com/prometheus/client_golang/prometheus" 28 | "github.com/prometheus/client_golang/prometheus/promhttp" 29 | "golang.org/x/exp/slog" 30 | ) 31 | 32 | // values populated by goreleaser 33 | var ( 34 | version = "dev" 35 | commit = "none" 36 | date = "2023-09-03T17:54:45Z" 37 | ) 38 | 39 | type buildInfo struct { 40 | App string 41 | Version string 42 | Date string 43 | Platform string 44 | Commit string 45 | GoVersion string 46 | } 47 | 48 | func closeHandler() { 49 | c := make(chan os.Signal, 1) 50 | signal.Notify(c, os.Interrupt) 51 | signal.Notify(c, syscall.SIGTERM) 52 | go func() { 53 | <-c 54 | slog.Warn("Shutting down", "signal", "Keyboard Interrupt", "input", "Ctrl+C") 55 | os.RemoveAll(pkg.CacheFolder) 56 | os.Exit(0) 57 | }() 58 | } 59 | 60 | // getbuildInfo constructs and returns a buildInfo struct containing metadata 61 | // about the application build, such as the application name, version, build date, 62 | // platform, commit hash, and Go runtime version. The build date is parsed and 63 | // reformatted to a human-readable format if it adheres to the RFC3339 standard. 64 | // If the date parsing fails, the original date string is used. 65 | func getbuildInfo() buildInfo { 66 | dt, err := time.Parse(time.RFC3339, date) 67 | if err == nil { 68 | date = dt.Format(time.UnixDate) 69 | } 70 | return buildInfo{ 71 | App: "AWS Quota Exporter (AQE)", 72 | Version: version, 73 | Date: date, 74 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 75 | Commit: commit, 76 | GoVersion: runtime.Version(), 77 | } 78 | } 79 | 80 | func printVersion() { 81 | appversion := getbuildInfo() 82 | fmt.Println(awsutil.Prettify(appversion)) 83 | } 84 | 85 | // buildInfoMetrics generates a slice of Prometheus metrics containing build information 86 | // about the application. It retrieves build details such as the application name, version, 87 | // build date, platform, commit hash, and Go version, and packages them into a Prometheus 88 | // metric with the name "aqe_build_info". 89 | // 90 | // Returns: 91 | // - A slice of pointers to PrometheusMetric containing the build information. 92 | // - An error if any issue occurs during the metric creation process. 93 | func buildInfoMetrics() ([]*pkg.PrometheusMetric, error) { 94 | appversion := getbuildInfo() 95 | var metrics []*pkg.PrometheusMetric 96 | 97 | labels := map[string]string{ 98 | "app": appversion.App, 99 | "version": appversion.Version, 100 | "build_date": appversion.Date, 101 | "platform": appversion.Platform, 102 | "commit": appversion.Commit, 103 | "go_version": appversion.GoVersion, 104 | } 105 | 106 | metrics = append(metrics, &pkg.PrometheusMetric{ 107 | Name: "aqe_build_info", 108 | Labels: labels, 109 | Value: 1, 110 | Desc: "AQE Build information", 111 | }) 112 | 113 | return metrics, nil 114 | } 115 | 116 | func main() { 117 | var ( 118 | configFile = flag.String("config.file", "/etc/aqe/config.yml", "Path to configuration file.") 119 | logFormatType = flag.String("log.format", "text", "Format of log messages (text or json).") 120 | logFolder = flag.String("log.folder", "stdout", "Folder to store logfiles. logs to stdout if not specified.") 121 | logLevel = flag.String("log.level", "INFO", "Log level to log from (DEBUG|INFO|WARN|ERROR).") 122 | promPort = flag.Int("prom.port", 10100, "Port to expose prometheus metrics.") 123 | cacheDuration = flag.Duration("cache.duration", 300*time.Second, "Cache expiry time.") 124 | cacheServeStale = flag.Bool("cache.serve-stale", false, "Serve stale cache data during cache refresh. This avoids delays in serving metrics. (default: false)") 125 | collectUsage = flag.Bool("collect.usage", false, "Collect quotas usage where available (NOTE: CloudWatch calls aren't free, default: false)") 126 | Version = flag.Bool("version", false, "Display aqe version") 127 | ) 128 | flag.Parse() 129 | 130 | if *Version { 131 | printVersion() 132 | os.Exit(0) 133 | } 134 | // create logger 135 | logger := pkg.NewLogger(*logFormatType, *logFolder, *logLevel).With("version", version) 136 | slog.SetDefault(logger) 137 | start := time.Now() 138 | slog.Info("Initializing AWS Quota Exporter") 139 | 140 | // Handle keyboard interrupt 141 | closeHandler() 142 | 143 | // Make Prometheus client aware of our collectors. 144 | qcl, err := pkg.NewQuotaConfig(*configFile) 145 | if err != nil { 146 | slog.Error(fmt.Sprintf("Error parsing '%s'", *configFile), "error", err) 147 | return 148 | } 149 | s, err := pkg.NewScraper() 150 | if err != nil { 151 | slog.Error("Error creating scraper", "error", err) 152 | return 153 | } 154 | 155 | reg := prometheus.NewRegistry() 156 | slog.Info("Registering scrappers") 157 | for _, job := range qcl.Jobs { 158 | 159 | pc := pkg.NewPrometheusCollector(s.CreateScraper(job, cacheDuration, *cacheServeStale, *collectUsage)) 160 | err = reg.Register(pc) 161 | if err != nil { 162 | slog.Error("Failed to register metrics: "+err.Error(), "serviceCode", job.ServiceCode, "regions", job.Regions, "role", job.Role) 163 | } 164 | } 165 | 166 | reg.Register(pkg.NewPrometheusCollector(buildInfoMetrics)) 167 | 168 | mux := http.NewServeMux() 169 | promHandler := promhttp.HandlerFor(reg, promhttp.HandlerOpts{}) 170 | mux.Handle("/metrics", promHandler) 171 | 172 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 173 | _, _ = w.Write([]byte(fmt.Sprintf(` 174 | AWS Quota Exporter 175 | 176 |

AWS Quota Exporter

177 | Version: %s 178 |

Metrics

179 | 180 | `, version))) 181 | }) 182 | 183 | mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { 184 | w.WriteHeader(http.StatusOK) 185 | _, _ = w.Write([]byte("ok")) 186 | }) 187 | 188 | slog.Info("Initialization of AWS Quota Exporter completed successfully", "duration", time.Since(start)) 189 | 190 | // Start listening for HTTP connections. 191 | port := fmt.Sprintf(":%d", *promPort) 192 | slog.Info("Starting AWS Quota Exporter", "address", fmt.Sprintf("%v/metrics", port)) 193 | if err := http.ListenAndServe(port, mux); err != nil { 194 | slog.Error("Cannot start AWS Quota Exporter", "error", err) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestHealthzHandler(t *testing.T) { 11 | req := httptest.NewRequest(http.MethodGet, "/healthz", nil) 12 | w := httptest.NewRecorder() 13 | writer := func(w http.ResponseWriter, r *http.Request) { 14 | w.WriteHeader(http.StatusOK) 15 | _, _ = w.Write([]byte("ok")) 16 | } 17 | writer(w, req) 18 | res := w.Result() 19 | defer res.Body.Close() 20 | data, err := io.ReadAll(res.Body) 21 | if err != nil { 22 | t.Errorf("expected error to be nil got %v", err) 23 | } 24 | if string(data) != "ok" { 25 | t.Errorf("expected 'ok' got %v", string(data)) 26 | } 27 | } 28 | func TestBuildInfoMetrics(t *testing.T) { 29 | metrics, err := buildInfoMetrics() 30 | if err != nil { 31 | t.Fatalf("expected no error, got %v", err) 32 | } 33 | 34 | if len(metrics) != 1 { 35 | t.Fatalf("expected 1 metric, got %d", len(metrics)) 36 | } 37 | 38 | metric := metrics[0] 39 | if metric.Name != "aqe_build_info" { 40 | t.Errorf("expected metric name 'aqe_build_info', got %s", metric.Name) 41 | } 42 | 43 | if metric.Value != 1 { 44 | t.Errorf("expected metric value 1, got %v", metric.Value) 45 | } 46 | 47 | expectedLabels := []string{"app", "version", "build_date", "platform", "commit", "go_version"} 48 | for _, label := range expectedLabels { 49 | if _, exists := metric.Labels[label]; !exists { 50 | t.Errorf("expected label '%s' to exist in metric labels", label) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/cache.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "os" 7 | "time" 8 | ) 9 | 10 | // Cache struct to manage the cache 11 | type Cache struct { 12 | FileName string 13 | LifeTime time.Duration 14 | Expires time.Time 15 | isEmpty bool 16 | ServeStale bool 17 | } 18 | 19 | var ( 20 | // ErrCacheExpired is returned when cache expires 21 | ErrCacheExpired = errors.New("Cache expired") 22 | // ErrCacheEmpty is returned when cache is empty 23 | ErrCacheEmpty = errors.New("Cache empty") 24 | // CacheFolder is a folder to store cache files 25 | CacheFolder = "/tmp/aws_quota_exporter_cache/" 26 | ) 27 | 28 | // NewCache creates a new Cache instance 29 | func NewCache(fileName string, lifeTime time.Duration) (*Cache, error) { 30 | // check if cache folder exists 31 | if _, err := os.Stat(CacheFolder); os.IsNotExist(err) { 32 | err = os.MkdirAll(CacheFolder, 0755) 33 | if err != nil { 34 | return nil, errors.New("Error creating cache folder: " + err.Error()) 35 | } 36 | } 37 | 38 | f, err := os.CreateTemp(CacheFolder, fileName+"-*.json") 39 | if err != nil { 40 | return nil, errors.New("Could not initialise cache for " + fileName + ": " + err.Error()) 41 | } 42 | 43 | return &Cache{ 44 | FileName: f.Name(), 45 | LifeTime: lifeTime, 46 | Expires: time.Now(), 47 | isEmpty: true, 48 | }, nil 49 | } 50 | 51 | // Read reads the contents of the cache file 52 | func (c *Cache) Read() ([]*PrometheusMetric, error) { 53 | if c.isEmpty { 54 | return nil, ErrCacheEmpty 55 | } 56 | byteData, err := os.ReadFile(c.FileName) 57 | if err != nil { 58 | return nil, err 59 | } 60 | var metrics []*PrometheusMetric 61 | err = json.Unmarshal(byteData, &metrics) 62 | if err != nil { 63 | return nil, err 64 | } 65 | if time.Now().After(c.Expires) { 66 | return metrics, ErrCacheExpired 67 | } 68 | 69 | return metrics, err 70 | } 71 | 72 | // Write writes data to the cache file 73 | func (c *Cache) Write(data []*PrometheusMetric) error { 74 | jsonData, err := json.Marshal(data) 75 | if err != nil { 76 | return err 77 | } 78 | err = os.WriteFile(c.FileName, jsonData, 0644) 79 | if err == nil { 80 | c.isEmpty = false 81 | c.ServeStale = false 82 | c.Expires = time.Now().Add(c.LifeTime) 83 | } 84 | return err 85 | } 86 | -------------------------------------------------------------------------------- /pkg/cache_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestNewCache(t *testing.T) { 10 | type args struct { 11 | fileName string 12 | lifeTime time.Duration 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want *Cache 18 | }{ 19 | { 20 | name: "test_cache", 21 | args: args{ 22 | fileName: "test_cache.json", 23 | lifeTime: time.Duration(1), 24 | }, 25 | want: &Cache{ 26 | FileName: CacheFolder + "test_cache.json", 27 | LifeTime: time.Second * time.Duration(1), 28 | Expires: time.Now(), 29 | isEmpty: true, 30 | }, 31 | }, 32 | } 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | var err error 36 | got, err := NewCache(tt.args.fileName, tt.args.lifeTime) 37 | 38 | if err != nil { 39 | t.Error("NewCache() errored out: ", err.Error()) 40 | } 41 | 42 | if !strings.HasPrefix(got.FileName, tt.want.FileName) { 43 | t.Errorf("NewCache() = %v, want %v", got, tt.want) 44 | } 45 | if got.isEmpty != tt.want.isEmpty { 46 | t.Errorf("NewCache() = %v, want %v", got, tt.want) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func FuzzNewCache(f *testing.F) { 53 | type args struct { 54 | fileName string 55 | lifeTime int 56 | } 57 | tests := []struct { 58 | name string 59 | args args 60 | want *Cache 61 | }{ 62 | { 63 | name: "test_cache1", 64 | args: args{ 65 | fileName: "test_cache1.json", 66 | lifeTime: 1, 67 | }, 68 | want: &Cache{ 69 | FileName: CacheFolder + "test_cache1.json", 70 | LifeTime: time.Second * time.Duration(1), 71 | Expires: time.Now(), 72 | isEmpty: true, 73 | }, 74 | }, 75 | { 76 | name: "test_cache2", 77 | args: args{ 78 | fileName: "test_cache2.json", 79 | lifeTime: 10, 80 | }, 81 | want: &Cache{ 82 | FileName: CacheFolder + "test_cache2.json", 83 | LifeTime: time.Second * time.Duration(10), 84 | Expires: time.Now(), 85 | isEmpty: true, 86 | }, 87 | }, 88 | } 89 | for _, tt := range tests { 90 | f.Add(tt.args.fileName, tt.args.lifeTime) 91 | } 92 | f.Fuzz( 93 | func(t *testing.T, filename string, lifetime int) { 94 | c, err := NewCache(filename, time.Duration(lifetime)) 95 | 96 | if err != nil { 97 | t.Error("NewCache() errored out:", err.Error()) 98 | } 99 | 100 | want := CacheFolder + filename 101 | if !strings.HasPrefix(c.FileName, want) { 102 | t.Errorf("NewCache().FileName = %v, want %v", c.FileName, want) 103 | } 104 | 105 | if c.isEmpty != true { 106 | t.Errorf("NewCache() = %v, want %v", c.isEmpty, true) 107 | } 108 | }, 109 | ) 110 | 111 | } 112 | -------------------------------------------------------------------------------- /pkg/collector.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "regexp" 5 | "sort" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/prometheus/client_golang/prometheus" 10 | "github.com/prometheus/common/model" 11 | "golang.org/x/exp/slog" 12 | ) 13 | 14 | var invalidPrometheusChars = regexp.MustCompile(`[^a-zA-Z0-9_]`) 15 | 16 | var splitRegexp = regexp.MustCompile(`([a-z0-9])([A-Z])`) 17 | var logGroup = slog.Group("request", 18 | slog.String("method", "GET"), 19 | slog.String("url", "/metrics"), 20 | ) 21 | 22 | // PrometheusMetric data structure 23 | type PrometheusMetric struct { 24 | Name string `json:"name"` 25 | Labels map[string]string `json:"labels"` 26 | Value float64 `json:"value"` 27 | Desc string `json:"desc"` 28 | } 29 | 30 | // PrometheusCollector Data structure 31 | type PrometheusCollector struct { 32 | mutex *sync.RWMutex 33 | getMetrics func() ([]*PrometheusMetric, error) 34 | } 35 | 36 | // NewPrometheusCollector is PrometheusCollector constructor 37 | func NewPrometheusCollector(getMetrics func() ([]*PrometheusMetric, error)) *PrometheusCollector { 38 | return &PrometheusCollector{ 39 | getMetrics: getMetrics, 40 | mutex: new(sync.RWMutex), 41 | } 42 | } 43 | 44 | // Describe metrics 45 | func (p *PrometheusCollector) Describe(descs chan<- *prometheus.Desc) { 46 | data, err := p.getMetrics() 47 | if err != nil { 48 | descs <- prometheus.NewInvalidDesc(err) 49 | slog.Error("Error getting metrics", logGroup, "error", err) 50 | return 51 | } 52 | for _, metric := range removeDuplicatedMetrics(data) { 53 | descs <- createDesc(metric) 54 | } 55 | } 56 | 57 | // Collect metrics 58 | func (p *PrometheusCollector) Collect(metrics chan<- prometheus.Metric) { 59 | p.mutex.Lock() // To protect metrics from concurrent collects. 60 | defer p.mutex.Unlock() 61 | 62 | data, err := p.getMetrics() 63 | if err != nil { 64 | desc := prometheus.NewDesc( 65 | "place_holder_prometheus_collector", 66 | "Help is not implemented yet", 67 | []string{}, 68 | nil, 69 | ) 70 | slog.Error("Error collecting metrics", logGroup, "error", err) 71 | metrics <- prometheus.NewInvalidMetric(desc, err) 72 | } 73 | for _, metric := range removeDuplicatedMetrics(data) { 74 | metrics <- createMetric(metric) 75 | } 76 | 77 | } 78 | 79 | func createDesc(metric *PrometheusMetric) *prometheus.Desc { 80 | return prometheus.NewDesc( 81 | metric.Name, 82 | metric.Desc, 83 | nil, 84 | metric.Labels, 85 | ) 86 | } 87 | 88 | func createMetric(metric *PrometheusMetric) prometheus.Metric { 89 | gauge := prometheus.NewGauge(prometheus.GaugeOpts{ 90 | Name: metric.Name, 91 | Help: metric.Desc, 92 | ConstLabels: metric.Labels, 93 | }) 94 | 95 | gauge.Set(metric.Value) 96 | 97 | return gauge 98 | } 99 | 100 | func removeDuplicatedMetrics(metrics []*PrometheusMetric) []*PrometheusMetric { 101 | keys := make(map[string]bool) 102 | filteredMetrics := []*PrometheusMetric{} 103 | for _, metric := range metrics { 104 | if metric != nil { 105 | check := metric.Name + combineLabels(metric.Labels) 106 | if _, value := keys[check]; !value { 107 | keys[check] = true 108 | filteredMetrics = append(filteredMetrics, metric) 109 | } 110 | } 111 | } 112 | return filteredMetrics 113 | } 114 | 115 | func combineLabels(labels map[string]string) string { 116 | var combinedLabels string 117 | keys := make([]string, 0, len(labels)) 118 | for k := range labels { 119 | keys = append(keys, k) 120 | } 121 | sort.Strings(keys) 122 | for _, k := range keys { 123 | combinedLabels += PromString(k) + PromString(labels[k]) 124 | } 125 | return combinedLabels 126 | } 127 | 128 | // PromString returns prometheus string representation 129 | func PromString(text string) string { 130 | text = splitString(text) 131 | return strings.ToLower(sanitize(text)) 132 | } 133 | 134 | // PromStringTag checks valid string 135 | func PromStringTag(text string, labelsSnakeCase bool) (bool, string) { 136 | var s string 137 | if labelsSnakeCase { 138 | s = PromString(text) 139 | } else { 140 | s = sanitize(text) 141 | } 142 | return model.LabelName(s).IsValid(), s 143 | } 144 | 145 | func sanitize(text string) string { 146 | return invalidPrometheusChars.ReplaceAllString(text, "_") 147 | } 148 | 149 | func splitString(text string) string { 150 | return splitRegexp.ReplaceAllString(text, `$1.$2`) 151 | } 152 | -------------------------------------------------------------------------------- /pkg/collector_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | func Test_createDesc(t *testing.T) { 10 | type args struct { 11 | metric *PrometheusMetric 12 | } 13 | metric := &PrometheusMetric{ 14 | Name: "test", 15 | Labels: map[string]string{"region": "us-east-1"}, 16 | Value: 50, 17 | Desc: "test description", 18 | } 19 | tests := []struct { 20 | name string 21 | args args 22 | want *prometheus.Desc 23 | }{ 24 | { 25 | name: "Describe", 26 | args: args{ 27 | metric: metric, 28 | }, 29 | want: prometheus.NewDesc( 30 | metric.Name, 31 | metric.Desc, 32 | nil, 33 | metric.Labels, 34 | ), 35 | }, 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | got := createDesc(tt.args.metric) 40 | if got.String() != tt.want.String() { 41 | t.Errorf("createDesc() = %v, want %v", got, tt.want) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestSanitize(t *testing.T) { 48 | testCases := []struct { 49 | input string 50 | expected string 51 | }{ 52 | {"allowed_only_09AZ", "allowed_only_09AZ"}, 53 | {"!@#$%^&*()'’", "____________"}, 54 | {"CamelCaseAllowedOnly", "CamelCaseAllowedOnly"}, 55 | } 56 | 57 | for _, tc := range testCases { 58 | t.Run(tc.input, func(t *testing.T) { 59 | result := sanitize(tc.input) 60 | if result != tc.expected { 61 | t.Errorf("Expected: %s, Got: %s", tc.expected, result) 62 | } 63 | }) 64 | } 65 | } 66 | 67 | func TestPromString(t *testing.T) { 68 | testCases := []struct { 69 | input string 70 | expected string 71 | }{ 72 | {"SomeText@Here123", "some_text_here123"}, 73 | {"!@#$%^&*()'’", "____________"}, 74 | } 75 | 76 | for _, tc := range testCases { 77 | t.Run(tc.input, func(t *testing.T) { 78 | result := PromString(tc.input) 79 | if result != tc.expected { 80 | t.Errorf("Expected: %s, Got: %s", tc.expected, result) 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/config.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/aws/aws-sdk-go/aws/awsutil" 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | // QuotaConfig struct contains Jobs 11 | type QuotaConfig struct { 12 | Jobs []JobConfig `yaml:"jobs"` 13 | } 14 | 15 | // JobConfig struct 16 | type JobConfig struct { 17 | ServiceCode string `yaml:"serviceCode"` 18 | Regions []string `yaml:"regions"` 19 | Role string `yaml:"role,omitempty"` 20 | AccountName string `yaml:"accountName,omitempty"` 21 | } 22 | 23 | // NewQuotaConfig creates a new QuotaConfig 24 | func NewQuotaConfig(configFile string) (*QuotaConfig, error) { 25 | yamlFile, err := os.ReadFile(configFile) 26 | if err != nil { 27 | return nil, err 28 | } 29 | qcl := QuotaConfig{} 30 | err = yaml.Unmarshal(yamlFile, &qcl) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return &qcl, nil 35 | } 36 | 37 | // String returns a string representation of QuotaConfig 38 | func (q *QuotaConfig) String() string { 39 | return awsutil.Prettify(q) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/config_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestNewQuotaConfig(t *testing.T) { 9 | type args struct { 10 | configFile string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want *QuotaConfig 16 | wantErr bool 17 | }{ 18 | { 19 | name: "Check config is loaded correctly", 20 | args: args{ 21 | configFile: "../docker/aws_quota_exporter/config.yml", 22 | }, 23 | want: &QuotaConfig{ 24 | Jobs: []JobConfig{ 25 | { 26 | ServiceCode: "lambda", 27 | Regions: []string{"us-west-2", "us-east-2"}, 28 | }, 29 | { 30 | ServiceCode: "cloudformation", 31 | Regions: []string{"us-west-2", "us-east-2"}, 32 | }, 33 | { 34 | ServiceCode: "ec2", 35 | Regions: []string{"us-west-2", "us-east-2"}, 36 | }, 37 | }, 38 | }, 39 | wantErr: false, 40 | }, 41 | { 42 | name: "wrong config path", 43 | args: args{ 44 | configFile: "wrong_path/config.yml", 45 | }, 46 | want: nil, 47 | wantErr: true, 48 | }, 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | got, err := NewQuotaConfig(tt.args.configFile) 53 | if (err != nil) != tt.wantErr { 54 | t.Errorf("NewQuotaConfig() error = %v, wantErr %v", err, tt.wantErr) 55 | return 56 | } 57 | 58 | if !reflect.DeepEqual(got, tt.want) { 59 | t.Errorf("NewQuotaConfig() = %v, want %v", got, tt.want) 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pkg/grouping.go: -------------------------------------------------------------------------------- 1 | // Package pkg grouping is used for grouping AWS service quotas and creating Prometheus metrics based on the grouped quotas. 2 | package pkg 3 | 4 | import ( 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/adrg/strutil/metrics" 10 | sqTypes "github.com/aws/aws-sdk-go-v2/service/servicequotas/types" 11 | "github.com/emylincon/golist" 12 | "golang.org/x/exp/slog" 13 | ) 14 | 15 | // MetricGroup represents a group of metrics. 16 | type MetricGroup struct { 17 | Label string `json:"label,omitempty"` // Label for the metric group. 18 | Common string `json:"common,omitempty"` // Common part of the metric name. 19 | Sim float64 `json:"sim,omitempty"` // Similarity score. 20 | Quota sqTypes.ServiceQuota `json:"quota,omitempty"` // AWS service quota. 21 | Usage float64 `json:"usage,omitempty"` // AWS service quota usage. 22 | } 23 | 24 | // Grouping represents a Grouping instance. 25 | type Grouping struct { 26 | maxSimilarity float64 // Maximum similarity score. 27 | region string // AWS region. 28 | account string // AWS account. 29 | accountName string // AWS account name. 30 | repl *regexp.Regexp // Regular expression for replacing patterns. 31 | } 32 | 33 | // NewGrouping initializes a new grouping instance. 34 | func NewGrouping(maxSimilarity float64, jobRegionCfg JobRegion) *Grouping { 35 | return &Grouping{ 36 | maxSimilarity: maxSimilarity, 37 | region: jobRegionCfg.Region, 38 | account: jobRegionCfg.AccountID, 39 | accountName: jobRegionCfg.AccountName, 40 | repl: regexp.MustCompile(` \(.*\)`), 41 | } 42 | } 43 | 44 | // diff computes the difference between two strings. 45 | func (g *Grouping) diff(a, b string) string { 46 | list := golist.NewList(strings.Split(a, " ")) 47 | other := golist.NewList(strings.Split(b, " ")) 48 | difference, err := list.Difference(other) 49 | if err != nil { 50 | slog.Debug("could not diff strings", "error", err) 51 | return "" 52 | } 53 | result, _ := difference.ConvertToSliceString() 54 | 55 | return strings.Join(result, " ") 56 | } 57 | 58 | // common computes the common parts between two strings. 59 | func (g *Grouping) common(a, b string) string { 60 | result := []string{} 61 | list := strings.Split(a, " ") 62 | other := golist.NewList(strings.Split(b, " ")) 63 | if list[0] != other.Get(0) { 64 | return "" 65 | } 66 | for _, item := range list { 67 | if other.Contains(item) { 68 | result = append(result, item) 69 | } 70 | } 71 | return strings.Join(result, " ") 72 | } 73 | 74 | // createPromMetric creates a Prometheus metric based on the given metric group and type. 75 | func (g *Grouping) createPromMetric(m MetricGroup, metricType string) *PrometheusMetric { 76 | value := *m.Quota.Value 77 | if metricType == "usage" { 78 | value = m.Usage 79 | } 80 | return &PrometheusMetric{ 81 | Name: createMetricName(*m.Quota.ServiceCode, m.Common), 82 | Value: value, 83 | Labels: map[string]string{ 84 | "type": metricType, 85 | "adjustable": strconv.FormatBool(m.Quota.Adjustable), 86 | "global_quota": strconv.FormatBool(m.Quota.GlobalQuota), 87 | "unit": *m.Quota.Unit, 88 | "region": g.region, 89 | "account": g.account, 90 | "account_name": g.accountName, 91 | "kind": m.Label, 92 | "name": *m.Quota.QuotaName, 93 | "quota_code": *m.Quota.QuotaCode, 94 | "service_code": *m.Quota.ServiceCode, 95 | }, 96 | Desc: createDescription(*m.Quota.ServiceName, m.Common), 97 | } 98 | } 99 | 100 | // RemoveBrackets removes brackets from metric names. 101 | func (g *Grouping) RemoveBrackets(str string) string { 102 | return g.repl.ReplaceAllString(str, "") 103 | } 104 | 105 | // GroupMetrics groups AWS service quotas. 106 | func (g *Grouping) GroupMetrics(quotas []QuotaUsage, collectUsage bool) (map[string][]MetricGroup, []*PrometheusMetric) { 107 | promMetrics := []*PrometheusMetric{} 108 | hem := metrics.NewLevenshtein() 109 | check := map[string]bool{} 110 | 111 | response := map[string][]MetricGroup{} 112 | 113 | for _, q := range quotas { 114 | if _, ok := check[*q.Quota.QuotaName]; ok { 115 | continue 116 | } 117 | check[*q.Quota.QuotaName] = true 118 | if len(response) == 0 { 119 | response[*q.Quota.QuotaName] = []MetricGroup{{Quota: q.Quota, Usage: q.Usage}} 120 | } else { 121 | selected := false 122 | for key := range response { 123 | sim := hem.Compare(g.RemoveBrackets(*q.Quota.QuotaName), g.RemoveBrackets(key)) 124 | if sim >= g.maxSimilarity { 125 | response[key] = append(response[key], MetricGroup{Quota: q.Quota, Usage: q.Usage}) 126 | if len(response[key]) == 2 { 127 | commonStr := g.common(*response[key][0].Quota.QuotaName, *response[key][1].Quota.QuotaName) 128 | if commonStr == "" || len(strings.Split(commonStr, " ")) <= 2 { // if the first words of metric names are not the same or common is only two words then skip 129 | response[key] = response[key][:len(response[key])-1] // remove added metric 130 | continue 131 | } 132 | for i := 0; i < 2; i++ { 133 | response[key][i].Label = g.diff(*response[key][i].Quota.QuotaName, *response[key][i^1].Quota.QuotaName) 134 | response[key][i].Common = commonStr 135 | response[key][i].Sim = hem.Compare(g.RemoveBrackets(*response[key][i].Quota.QuotaName), g.RemoveBrackets(key)) 136 | promMetrics = append(promMetrics, g.createPromMetric(response[key][i], "quota")) 137 | if collectUsage && response[key][i].Quota.UsageMetric != nil { // add Usage metric if Quota has UsageMetric 138 | promMetrics = append(promMetrics, g.createPromMetric(response[key][i], "usage")) 139 | } 140 | } 141 | } else if len(response[key]) > 2 { 142 | _id := len(response[key]) - 1 143 | if strings.Split(key, " ")[0] != strings.Split(*response[key][_id].Quota.QuotaName, " ")[0] { // if the first words of metric names are not the same then skip 144 | continue 145 | } 146 | response[key][_id].Label = g.diff(*response[key][_id].Quota.QuotaName, key) 147 | response[key][_id].Common = response[key][0].Common 148 | response[key][_id].Sim = sim 149 | promMetrics = append(promMetrics, g.createPromMetric(response[key][_id], "quota")) 150 | if collectUsage && response[key][_id].Quota.UsageMetric != nil { // add Usage metric if Quota has UsageMetric 151 | promMetrics = append(promMetrics, g.createPromMetric(response[key][_id], "usage")) 152 | } 153 | } 154 | selected = true 155 | break 156 | } 157 | } 158 | if !selected { 159 | response[*q.Quota.QuotaName] = []MetricGroup{{Quota: q.Quota, Usage: q.Usage}} 160 | } 161 | } 162 | } 163 | return response, promMetrics 164 | } 165 | -------------------------------------------------------------------------------- /pkg/grouping_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | sqTypes "github.com/aws/aws-sdk-go-v2/service/servicequotas/types" 8 | "github.com/aws/aws-sdk-go/aws/awsutil" 9 | ) 10 | 11 | func TestGroupMetrics(t *testing.T) { 12 | 13 | tests := []struct { 14 | name string 15 | groupsLength int 16 | promLength int 17 | collectUsage bool 18 | quotas []QuotaUsage 19 | }{ 20 | { 21 | name: "common with only 2 words", 22 | groupsLength: 3, 23 | promLength: 0, 24 | quotas: []QuotaUsage{ 25 | {Quota: sqTypes.ServiceQuota{ 26 | ServiceCode: Ptr("ec2"), 27 | ServiceName: Ptr("Amazon Elastic Compute Cloud"), 28 | QuotaName: Ptr("Test Quota 1"), 29 | QuotaCode: Ptr("L-12345"), 30 | Value: Ptr(100.0), 31 | Adjustable: true, 32 | GlobalQuota: false, 33 | Unit: Ptr("Count"), 34 | UsageMetric: nil, 35 | Period: nil, 36 | ErrorReason: nil, 37 | }}, 38 | {Quota: sqTypes.ServiceQuota{ 39 | ServiceCode: Ptr("ec2"), 40 | ServiceName: Ptr("Amazon Elastic Compute Cloud"), 41 | QuotaName: Ptr("Test Quota 2"), 42 | QuotaCode: Ptr("L-12346"), 43 | Value: Ptr(100.0), 44 | Adjustable: true, 45 | GlobalQuota: false, 46 | Unit: Ptr("Count"), 47 | UsageMetric: nil, 48 | Period: nil, 49 | ErrorReason: nil, 50 | }}, 51 | {Quota: sqTypes.ServiceQuota{ 52 | ServiceCode: Ptr("ec2"), 53 | ServiceName: Ptr("Amazon Elastic Compute Cloud"), 54 | QuotaName: Ptr("Test Quota 3"), 55 | QuotaCode: Ptr("L-12347"), 56 | Value: Ptr(100.0), 57 | Adjustable: true, 58 | GlobalQuota: false, 59 | Unit: Ptr("Count"), 60 | UsageMetric: nil, 61 | Period: nil, 62 | ErrorReason: nil, 63 | }}, 64 | }, 65 | }, 66 | { 67 | name: "common with more than 2 words", 68 | groupsLength: 1, 69 | promLength: 3, 70 | quotas: []QuotaUsage{ 71 | {Quota: sqTypes.ServiceQuota{ 72 | ServiceCode: Ptr("ec2"), 73 | ServiceName: Ptr("Amazon Elastic Compute Cloud"), 74 | QuotaName: Ptr("All DL Spot Instance Requests"), 75 | QuotaCode: Ptr("L-12345"), 76 | Value: Ptr(100.0), 77 | Adjustable: true, 78 | GlobalQuota: false, 79 | Unit: Ptr("Count"), 80 | UsageMetric: nil, 81 | Period: nil, 82 | ErrorReason: nil, 83 | }}, 84 | {Quota: sqTypes.ServiceQuota{ 85 | ServiceCode: Ptr("ec2"), 86 | ServiceName: Ptr("Amazon Elastic Compute Cloud"), 87 | QuotaName: Ptr("All F Spot Instance Requests"), 88 | QuotaCode: Ptr("L-12346"), 89 | Value: Ptr(100.0), 90 | Adjustable: true, 91 | GlobalQuota: false, 92 | Unit: Ptr("Count"), 93 | UsageMetric: nil, 94 | Period: nil, 95 | ErrorReason: nil, 96 | }}, 97 | {Quota: sqTypes.ServiceQuota{ 98 | ServiceCode: Ptr("ec2"), 99 | ServiceName: Ptr("Amazon Elastic Compute Cloud"), 100 | QuotaName: Ptr("All Standard (A, C, D, H, I, M, R, T, Z) Spot Instance Requests"), 101 | QuotaCode: Ptr("L-12347"), 102 | Value: Ptr(100.0), 103 | Adjustable: true, 104 | GlobalQuota: false, 105 | Unit: Ptr("Count"), 106 | UsageMetric: nil, 107 | Period: nil, 108 | ErrorReason: nil, 109 | }}, 110 | }, 111 | }, 112 | } 113 | 114 | maxSimilarity := 0.5 115 | jobRegionCfg := JobRegion{ 116 | Region: "us-west-2", 117 | AccountName: "dev-account", 118 | AccountID: "123456789012", 119 | } 120 | 121 | grouping := NewGrouping(maxSimilarity, jobRegionCfg) 122 | 123 | for _, tt := range tests { 124 | t.Run(tt.name, func(t *testing.T) { 125 | groups, promMetrics := grouping.GroupMetrics(tt.quotas, tt.collectUsage) 126 | if len(groups) != tt.groupsLength { 127 | fmt.Println("groups=", awsutil.Prettify(groups), "prom", awsutil.Prettify(promMetrics)) 128 | t.Errorf("Expected %d groups, got %d", tt.groupsLength, len(groups)) 129 | } 130 | 131 | if len(promMetrics) != tt.promLength { 132 | fmt.Println("prom=", awsutil.Prettify(promMetrics)) 133 | t.Errorf("Expected %d Prometheus metrics, got %d", tt.promLength, len(promMetrics)) 134 | } 135 | }) 136 | } 137 | 138 | } 139 | 140 | type Any interface { 141 | string | float64 | bool 142 | } 143 | 144 | // Ptr returns a pointer of given parameter 145 | func Ptr[T Any](any T) *T { 146 | return &any 147 | } 148 | -------------------------------------------------------------------------------- /pkg/log.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "golang.org/x/exp/slog" 11 | ) 12 | 13 | // NewLogWriter creates a new LogWriter 14 | func NewLogWriter(folder string) (*LogWriter, error) { 15 | // check if cache folder exists 16 | if _, err := os.Stat(folder); os.IsNotExist(err) { 17 | err = os.MkdirAll(folder, 0755) 18 | if err != nil { 19 | return nil, err 20 | } 21 | } 22 | lw := LogWriter{ 23 | logFolder: folder, 24 | } 25 | lw.setlogFile() 26 | return &lw, nil 27 | 28 | } 29 | 30 | // LogWriter is a wrapper around os.File 31 | type LogWriter struct { 32 | logFolder string 33 | logFile string 34 | expires time.Time 35 | } 36 | 37 | // Write implements io.Writer 38 | func (lw *LogWriter) Write(b []byte) (n int, err error) { 39 | if time.Now().After(lw.expires) { 40 | lw.setlogFile() 41 | } 42 | f, err := os.OpenFile(lw.getlogFile(), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) 43 | if err != nil { 44 | fmt.Printf("Error opening log file - %s: error=%s\n", lw.getlogFile(), err) 45 | fmt.Println("Switching to default console logging") 46 | slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout))) 47 | slog.Info("Process recovered successfully from error while writing logs", "error", err) 48 | } 49 | defer f.Close() 50 | 51 | return f.Write(b) 52 | } 53 | 54 | func (lw *LogWriter) setlogFile() { 55 | lw.logFile = fmt.Sprintf("aws-quota-exporter-%s.log", time.Now().Format("02-01-2006")) 56 | lw.expires = time.Now().Add(time.Hour * 24) 57 | } 58 | 59 | func (lw *LogWriter) getlogFile() string { 60 | return fmt.Sprintf("%s/%s", lw.logFolder, lw.logFile) 61 | } 62 | 63 | // NewLogger returns a logger 64 | func NewLogger(formatType, logFolder, logLevel string) *slog.Logger { 65 | LogLevel := new(slog.LevelVar) 66 | switch strings.ToUpper(logLevel) { 67 | case "DEBUG": 68 | LogLevel.Set(slog.LevelDebug) 69 | case "INFO": 70 | LogLevel.Set(slog.LevelInfo) 71 | case "WARN": 72 | LogLevel.Set(slog.LevelWarn) 73 | case "ERROR": 74 | LogLevel.Set(slog.LevelError) 75 | } 76 | logOptions := slog.HandlerOptions{Level: LogLevel} 77 | var logwriter io.Writer 78 | logwriter = os.Stdout 79 | if logFolder != "stdout" { 80 | writer, err := NewLogWriter(logFolder) 81 | if err != nil { 82 | slog.Error("Error creating log folder", "error", err) 83 | slog.Warn("Switching to writing logs to console") 84 | } else { 85 | logwriter = writer 86 | } 87 | 88 | } 89 | if formatType == "json" { 90 | handler := logOptions.NewJSONHandler(logwriter) 91 | return slog.New(handler) 92 | } 93 | handler := logOptions.NewTextHandler(logwriter) 94 | return slog.New(handler) 95 | } 96 | -------------------------------------------------------------------------------- /pkg/log_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | 9 | "golang.org/x/exp/slog" 10 | ) 11 | 12 | func TestNewLogger(t *testing.T) { 13 | type args struct { 14 | formatType string 15 | logFolder string 16 | logLevel string 17 | } 18 | var LogLevel = new(slog.LevelVar) 19 | tests := []struct { 20 | name string 21 | args args 22 | want *slog.Logger 23 | }{ 24 | { 25 | name: "json_handler_debug", 26 | args: args{ 27 | formatType: "json", 28 | logFolder: "stdout", 29 | logLevel: "debug", 30 | }, 31 | want: slog.New(slog.HandlerOptions{Level: LogLevel}.NewJSONHandler(os.Stdout)), 32 | }, 33 | { 34 | name: "json_handler_info", 35 | args: args{ 36 | formatType: "json", 37 | logFolder: "stdout", 38 | logLevel: "info", 39 | }, 40 | want: slog.New(slog.HandlerOptions{Level: LogLevel}.NewJSONHandler(os.Stdout)), 41 | }, 42 | { 43 | name: "text_handler_debug", 44 | args: args{ 45 | formatType: "text", 46 | logFolder: "stdout", 47 | logLevel: "debug", 48 | }, 49 | want: slog.New(slog.HandlerOptions{Level: LogLevel}.NewTextHandler(os.Stdout)), 50 | }, 51 | { 52 | name: "text_handler_info", 53 | args: args{ 54 | formatType: "text", 55 | logFolder: "stdout", 56 | logLevel: "info", 57 | }, 58 | want: slog.New(slog.HandlerOptions{Level: LogLevel}.NewTextHandler(os.Stdout)), 59 | }, 60 | } 61 | for _, tt := range tests { 62 | t.Run(tt.name, func(t *testing.T) { 63 | switch strings.ToUpper(tt.args.logLevel) { 64 | case "DEBUG": 65 | LogLevel.Set(slog.LevelDebug) 66 | case "INFO": 67 | LogLevel.Set(slog.LevelInfo) 68 | case "WARN": 69 | LogLevel.Set(slog.LevelWarn) 70 | case "ERROR": 71 | LogLevel.Set(slog.LevelError) 72 | } 73 | if got := NewLogger(tt.args.formatType, tt.args.logFolder, tt.args.logLevel); !reflect.DeepEqual(got, tt.want) { 74 | t.Errorf("NewLogger() = %v, want %v", got, tt.want) 75 | } 76 | }) 77 | } 78 | } 79 | 80 | func TestNewLogWriter(t *testing.T) { 81 | type args struct { 82 | folder string 83 | } 84 | tests := []struct { 85 | name string 86 | args args 87 | want *LogWriter 88 | wantErr bool 89 | }{ 90 | { 91 | name: "test empty folder", 92 | args: args{ 93 | folder: "", 94 | }, 95 | want: nil, 96 | wantErr: true, 97 | }, 98 | } 99 | for _, tt := range tests { 100 | t.Run(tt.name, func(t *testing.T) { 101 | got, err := NewLogWriter(tt.args.folder) 102 | if (err != nil) != tt.wantErr { 103 | t.Errorf("NewLogWriter() error = %v, wantErr %v", err, tt.wantErr) 104 | return 105 | } 106 | if !reflect.DeepEqual(got, tt.want) { 107 | t.Errorf("NewLogWriter() = %v, want %v", got, tt.want) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /pkg/scrapper_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/aws/aws-sdk-go-v2/config" 12 | cw "github.com/aws/aws-sdk-go-v2/service/cloudwatch" 13 | cwTypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" 14 | sqTypes "github.com/aws/aws-sdk-go-v2/service/servicequotas/types" 15 | ) 16 | 17 | type MockCloudWatchClient struct { 18 | CloudWatchClient 19 | } 20 | 21 | func (m *MockCloudWatchClient) GetMetricStatistics(ctx context.Context, params *cw.GetMetricStatisticsInput, optFns ...func(*cw.Options)) (*cw.GetMetricStatisticsOutput, error) { 22 | return &cw.GetMetricStatisticsOutput{ 23 | Datapoints: []cwTypes.Datapoint{ 24 | { 25 | Average: aws.Float64(50), 26 | }, 27 | }, 28 | }, nil 29 | } 30 | 31 | func TestNewScraper(t *testing.T) { 32 | cfg, _ := config.LoadDefaultConfig(context.TODO()) 33 | tests := []struct { 34 | name string 35 | want *Scraper 36 | wantErr bool 37 | }{ 38 | { 39 | name: "test New Scraper", 40 | want: &Scraper{ 41 | cfg: cfg, 42 | }, 43 | wantErr: false, 44 | }, 45 | } 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | got, err := NewScraper() 49 | if (err != nil) != tt.wantErr { 50 | t.Errorf("NewScraper() error = %v, wantErr %v", err, tt.wantErr) 51 | return 52 | } 53 | 54 | if !reflect.DeepEqual(got.cfg.Region, tt.want.cfg.Region) { 55 | t.Errorf("NewScraper() = %v, want %v", got, tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestScraper_CreateScraper(t *testing.T) { 62 | type fields struct { 63 | cfg aws.Config 64 | } 65 | type args struct { 66 | job JobConfig 67 | cacheExpiryDuration time.Duration 68 | collectUsage bool 69 | serveStale bool 70 | } 71 | cfg, _ := config.LoadDefaultConfig(context.TODO()) 72 | failedServiceQuota := errors.New("operation error Service Quotas: ListServiceQuotas, failed to sign request: failed to retrieve credentials: failed to refresh cached credentials, no EC2 IMDS role found, operation error ec2imds: GetMetadata, request canceled, context deadline exceeded") 73 | tests := []struct { 74 | name string 75 | fields fields 76 | args args 77 | want func() ([]*PrometheusMetric, error) 78 | wantErr bool 79 | }{ 80 | { 81 | name: "test create scrapper", 82 | fields: fields{ 83 | cfg: cfg, 84 | }, 85 | args: args{ 86 | job: JobConfig{Regions: []string{"us-west-2"}, ServiceCode: "lambda"}, 87 | cacheExpiryDuration: time.Duration(1) * time.Hour, 88 | }, 89 | want: func() ([]*PrometheusMetric, error) { 90 | return nil, failedServiceQuota 91 | }, 92 | wantErr: true, 93 | }, 94 | } 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | s := &Scraper{ 98 | cfg: tt.fields.cfg, 99 | } 100 | 101 | got := s.CreateScraper(tt.args.job, &tt.args.cacheExpiryDuration, tt.args.serveStale, tt.args.collectUsage) 102 | d, derr := got() 103 | r, terr := tt.want() 104 | if (derr != nil) != tt.wantErr { 105 | t.Errorf("Scraper.CreateScraper() error = %v, wantErr %v", derr, tt.wantErr) 106 | return 107 | } 108 | 109 | if !reflect.DeepEqual(d, r) { 110 | t.Errorf("Scraper.CreateScraper() = %v:%v, want %v:%v", d, derr, r, terr) 111 | } 112 | }) 113 | } 114 | } 115 | 116 | func Test_validateRoleARN(t *testing.T) { 117 | type args struct { 118 | role string 119 | } 120 | tests := []struct { 121 | name string 122 | args args 123 | want bool 124 | }{ 125 | { 126 | name: "Valid role ARN", 127 | args: args{role: "arn:aws:iam::012345678901:role/aws-quota-exporter"}, 128 | want: true, 129 | }, 130 | { 131 | name: "Invalid role ARN", 132 | args: args{role: "arn:aws:iam::012345678901:user/aws-quota-exporter"}, 133 | want: false, 134 | }, 135 | { 136 | name: "Not an ARN", 137 | args: args{role: "foo"}, 138 | want: false, 139 | }, 140 | } 141 | for _, tt := range tests { 142 | t.Run(tt.name, func(t *testing.T) { 143 | if got := validateRoleARN(tt.args.role); got != tt.want { 144 | t.Errorf("validateRoleARN() = %v, want %v", got, tt.want) 145 | } 146 | }) 147 | } 148 | } 149 | func Test_getQuotasUsage(t *testing.T) { 150 | type args struct { 151 | ctx context.Context 152 | quotas []sqTypes.ServiceQuota 153 | region string 154 | } 155 | tests := []struct { 156 | name string 157 | args args 158 | want []QuotaUsage 159 | }{ 160 | { 161 | name: "Test with empty quotas", 162 | args: args{ 163 | ctx: context.TODO(), 164 | quotas: []sqTypes.ServiceQuota{}, 165 | region: "us-west-2", 166 | }, 167 | want: []QuotaUsage{}, 168 | }, 169 | { 170 | name: "Test with quotas without usage metrics", 171 | args: args{ 172 | ctx: context.TODO(), 173 | quotas: []sqTypes.ServiceQuota{ 174 | { 175 | ServiceCode: aws.String("test"), 176 | QuotaCode: aws.String("L-12345"), 177 | QuotaName: aws.String("Test Quota"), 178 | Value: aws.Float64(100), 179 | }, 180 | }, 181 | region: "us-west-2", 182 | }, 183 | want: []QuotaUsage{ 184 | { 185 | Quota: sqTypes.ServiceQuota{ 186 | ServiceCode: aws.String("test"), 187 | QuotaCode: aws.String("L-12345"), 188 | QuotaName: aws.String("Test Quota"), 189 | Value: aws.Float64(100), 190 | }, 191 | Usage: 0, 192 | }, 193 | }, 194 | }, 195 | { 196 | name: "Test with quotas with usage metrics", 197 | args: args{ 198 | ctx: context.TODO(), 199 | quotas: []sqTypes.ServiceQuota{ 200 | { 201 | ServiceCode: aws.String("test"), 202 | QuotaCode: aws.String("L-12345"), 203 | QuotaName: aws.String("Test Quota"), 204 | Value: aws.Float64(100), 205 | UsageMetric: &sqTypes.MetricInfo{ 206 | MetricName: aws.String("CPUUtilization"), 207 | MetricNamespace: aws.String("AWS/EC2"), 208 | MetricDimensions: map[string]string{"InstanceId": "i-1234567890abcdef0"}, 209 | MetricStatisticRecommendation: aws.String("Average"), 210 | }, 211 | }, 212 | }, 213 | region: "us-west-2", 214 | }, 215 | want: []QuotaUsage{ 216 | { 217 | Quota: sqTypes.ServiceQuota{ 218 | ServiceCode: aws.String("test"), 219 | QuotaCode: aws.String("L-12345"), 220 | QuotaName: aws.String("Test Quota"), 221 | Value: aws.Float64(100), 222 | UsageMetric: &sqTypes.MetricInfo{ 223 | MetricName: aws.String("CPUUtilization"), 224 | MetricNamespace: aws.String("AWS/EC2"), 225 | MetricDimensions: map[string]string{"InstanceId": "i-1234567890abcdef0"}, 226 | MetricStatisticRecommendation: aws.String("Average"), 227 | }, 228 | }, 229 | Usage: 50, // This should be set to the actual usage value from the mock CloudWatch client 230 | }, 231 | }, 232 | }, 233 | } 234 | 235 | for _, tt := range tests { 236 | t.Run(tt.name, func(t *testing.T) { 237 | mockCWClient := &MockCloudWatchClient{} 238 | got := getQuotasUsage(tt.args.ctx, tt.args.quotas, mockCWClient, tt.args.region) 239 | if !reflect.DeepEqual(got, tt.want) { 240 | t.Errorf("getQuotasUsage() = %v, want %v", got, tt.want) 241 | } 242 | }) 243 | } 244 | } 245 | --------------------------------------------------------------------------------